개인 개발 공부 일지

Thread 사용법 및 코드예제

everydeveloper 2024. 6. 13. 20:02

스레드(Thread)는 여러 작업을 동시에 수행하거나 병렬로 처리할 때 사용되는 중요한 개념입니다. 자바에서 스레드를 사용하는 방법과 관련된 내용을 상세히 설명하고, 실제로 어떻게 구현하는지 예제를 통해 설명하겠습니다.

스레드의 기본 개념

스레드는 프로세스 내에서 독립적으로 실행되는 흐름의 단위입니다. 프로세스는 운영 체제에 의해 실행되는 프로그램을 의미하며, 스레드는 그 프로세스 안에서 병렬적으로 작업을 수행할 수 있게 해줍니다. 스레드를 이용하면 멀티태스킹이 가능해져서 시스템 자원을 효율적으로 사용할 수 있습니다.

스레드 생성 방법

자바에서 스레드를 생성하는 방법은 크게 두 가지가 있습니다:

  1. Thread 클래스를 상속받는 방법
  2. Runnable 인터페이스를 구현하는 방법

1. Thread 클래스를 상속받는 방법

Thread 클래스를 상속받아 스레드를 생성할 수 있습니다. 이 방법은 run() 메서드를 오버라이딩하여 작업을 정의합니다.

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
            try {
                Thread.sleep(1000); // 1초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start();
        thread2.start();
    }
}

2. Runnable 인터페이스를 구현하는 방법

Runnable 인터페이스를 구현하여 스레드를 생성할 수 있습니다. 이 방법은 run() 메서드를 구현하고, 이를 Thread 객체에 전달하여 실행합니다.

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
            try {
                Thread.sleep(1000); // 1초 대기
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable());
        Thread thread2 = new Thread(new MyRunnable());

        thread1.start();
        thread2.start();
    }
}

스레드 동기화

여러 스레드가 동시에 실행될 때, 동일한 자원에 접근하면 문제(데이터 일관성 문제)가 발생할 수 있습니다. 이를 해결하기 위해 스레드 동기화를 사용합니다.

동기화 메서드

메서드에 synchronized 키워드를 사용하여 동기화를 할 수 있습니다.

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

class CounterThread extends Thread {
    private Counter counter;

    public CounterThread(Counter counter) {
        this.counter = counter;
    }

    public void run() {
        for (int i = 0; i < 1000; i++) {
            counter.increment();
        }
    }
}

public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        CounterThread thread1 = new CounterThread(counter);
        CounterThread thread2 = new CounterThread(counter);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

위 예제에서는 increment() 메서드에 synchronized 키워드를 사용하여 여러 스레드가 동시에 접근할 수 없도록 했습니다.

스레드 상태

스레드는 생명주기 동안 여러 상태를 가질 수 있습니다:

  1. New: 스레드가 생성되었으나 아직 시작되지 않은 상태.
  2. Runnable: 스레드가 실행 중이거나 실행 준비가 된 상태.
  3. Blocked: 스레드가 실행 중에 잠금이 걸려 대기 중인 상태.
  4. Waiting: 스레드가 다른 스레드의 작업을 기다리는 상태.
  5. Timed Waiting: 주어진 시간 동안 대기하는 상태.
  6. Terminated: 스레드가 실행을 마친 상태.

이 상태들은 Thread.State enum을 통해 확인할 수 있습니다.

public class ThreadStateExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println("Thread state: " + thread.getState());
        thread.start();
        System.out.println("Thread state: " + thread.getState());

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Thread state: " + thread.getState());
    }
}

이 코드를 통해 스레드의 상태 변화를 관찰할 수 있습니다.

결론

스레드는 자바 프로그래밍에서 중요한 역할을 하며, 여러 작업을 동시에 수행할 수 있게 해줍니다. 위에서 설명한 방법들로 스레드를 생성하고 동기화하는 방법을 익히고, 다양한 예제를 통해 이해를 높이길 바랍니다. 스레드 프로그래밍은 복잡하지만, 적절히 사용하면 성능 향상과 효율적인 자원 관리를 가능하게 합니다.

혹시 이런 내용 내가 저번에 물어보고 또 공부해서 그런데 다른 내용이나 더 깊은 내용 없어?

물론입니다. 더 깊이 있는 스레드와 관련된 내용으로, 자바에서 자주 사용되는 고급 스레드 관리 기법과 병렬 프로그래밍 기법들을 다뤄보겠습니다.

고급 스레드 관리 기법

  1. Executor Framework
  2. Fork/Join Framework
  3. Java Concurrency Utilities

1. Executor Framework

Executor Framework는 스레드 풀을 관리하고 작업을 제출할 수 있는 방법을 제공합니다. 이는 직접 스레드를 생성하고 관리하는 것보다 훨씬 더 효율적입니다.

  • Executor: 작업을 실행할 수 있는 단순한 인터페이스.
  • ExecutorService: 종료, 결과 처리, 여러 작업 제출 등의 기능을 제공하는 인터페이스.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExecutorExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task executed by " + threadName);
            });
        }

        executorService.shutdown();
        try {
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
    }
}

2. Fork/Join Framework

Fork/Join Framework는 큰 작업을 작은 작업으로 나누어 병렬로 처리할 수 있게 해줍니다. 이는 주로 재귀적 알고리즘에 사용됩니다.

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class Fibonacci extends RecursiveTask<Integer> {
        final int n;

        Fibonacci(int n) {
            this.n = n;
        }

        @Override
        protected Integer compute() {
            if (n <= 1) return n;
            Fibonacci f1 = new Fibonacci(n - 1);
            Fibonacci f2 = new Fibonacci(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int result = pool.invoke(new Fibonacci(10));
        System.out.println("Fibonacci(10) = " + result);
    }
}

3. Java Concurrency Utilities

Java는 병렬 프로그래밍을 위한 다양한 유틸리티 클래스를 제공합니다. 그 중 몇 가지 중요한 클래스는 다음과 같습니다:

  • CountDownLatch: 한 스레드가 다른 스레드의 작업이 완료될 때까지 기다리게 할 수 있습니다.
  • CyclicBarrier: 여러 스레드가 일정 시점까지 모두 모일 때까지 기다리게 할 수 있습니다.
  • Semaphore: 지정된 수의 스레드만 특정 작업을 수행할 수 있도록 제한합니다.
  • Exchanger: 두 스레드가 서로 데이터를 교환할 수 있도록 합니다.

CountDownLatch 예제

import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName() + " is running");
            latch.countDown();
        };

        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }

        latch.await(); // Main thread waits here until count reaches zero
        System.out.println("All tasks are completed");
    }
}

고급 스레드 관리 예제

비동기 작업 처리 (CompletableFuture)

CompletableFuture는 비동기 프로그래밍을 간편하게 할 수 있는 클래스입니다. 비동기 작업을 체인 형태로 연결하여 실행할 수 있습니다.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(1000); // Simulate long running task
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello";
        });

        CompletableFuture<String> result = future.thenApply(s -> s + " World");

        System.out.println(result.get()); // Output: Hello World
    }
}

스레드 풀 관리

스레드 풀은 스레드를 재사용하여 자원을 효율적으로 관리하는 데 유용합니다. Executors 클래스를 통해 다양한 스레드 풀을 생성할 수 있습니다.

  • Fixed Thread Pool: 고정된 크기의 스레드 풀 생성.
  • Cached Thread Pool: 필요에 따라 스레드를 생성하고, 사용되지 않은 스레드를 캐시하여 재사용.
  • Single Thread Executor: 단일 스레드로 작업을 처리.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(2);
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();
            System.out.println("Executing task in " + threadName);
        };

        fixedThreadPool.submit(task);
        cachedThreadPool.submit(task);
        singleThreadExecutor.submit(task);

        fixedThreadPool.shutdown();
        cachedThreadPool.shutdown();
        singleThreadExecutor.shutdown();
    }
}

고급 동기화 기법

ReentrantLock

ReentrantLock은 synchronized 블록보다 더 많은 기능을 제공하는 클래스입니다. 예를 들어, 락을 공정하게 획득하도록 설정할 수 있으며, 락 상태를 쿼리할 수 있습니다.

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void doWork() {
        lock.lock();
        try {
            // critical section
            System.out.println(Thread.currentThread().getName() + " is working");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();
        Runnable task = example::doWork;

        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
    }
}

결론

이러한 고급 스레드 관리 기법과 병렬 프로그래밍 기법을 통해 자바 애플리케이션의 성능과 효율성을 크게 향상시킬 수 있습니다. 멀티스레딩과 병렬 처리는 복잡하지만, 적절히 활용하면 강력한 도구가 됩니다. 다양한 유틸리티와 프레임워크를 사용하여 복잡한 동시성 문제를 효과적으로 해결할 수 있습니다.

라이브러리가 아니라 프레임 워크 레벨이 필요하네?

알겠습니다. 프레임워크 수준의 멀티스레딩 및 병렬 프로그래밍 관련 내용을 다루겠습니다. 프레임워크 레벨에서 고급 병렬 프로그래밍을 지원하는 대표적인 프레임워크로는 Akka와 Vert.x가 있습니다.

Akka

Akka는 JVM 기반의 분산 컴퓨팅 프레임워크로, 액터 모델을 사용하여 동시성 문제를 해결합니다. Akka를 사용하면 메시지 기반의 비동기 프로그래밍을 간편하게 구현할 수 있습니다.

Akka의 주요 개념

  1. Actor: 상태를 갖고 있는 객체로, 메시지를 통해 상호작용합니다.
  2. ActorSystem: 액터들을 관리하고 메시지를 전달합니다.
  3. Dispatcher: 메시지를 비동기적으로 처리합니다.

Akka 사용 예제

먼저, Akka를 사용하기 위해 build.sbt 파일에 Akka 라이브러리를 추가합니다.

libraryDependencies += "com.typesafe.akka" %% "akka-actor" % "2.6.19"

액터를 정의하고 메시지를 처리하는 간단한 예제를 살펴보겠습니다.

import akka.actor.{Actor, ActorSystem, Props}

case object Greet
case class WhoToGreet(who: String)

class Greeter extends Actor {
  var greeting = ""

  def receive = {
    case WhoToGreet(who) => greeting = s"hello, $who"
    case Greet           => println(greeting)
  }
}

object AkkaQuickstart extends App {
  val system: ActorSystem = ActorSystem("helloAkka")
  val greeter = system.actorOf(Props[Greeter], "greeter")

  greeter ! WhoToGreet("Akka")
  greeter ! Greet

  system.terminate()
}

이 예제에서는 Greeter 액터를 정의하고, 두 가지 메시지(WhoToGreet와 Greet)를 처리합니다. ActorSystem을 통해 액터를 생성하고 메시지를 전송합니다.

Vert.x

Vert.x는 이벤트 기반의 비동기 프레임워크로, 고성능의 비동기 애플리케이션을 작성하는 데 사용됩니다. Vert.x는 특히 비동기 I/O 및 분산 애플리케이션에 강력합니다.

Vert.x의 주요 개념

  1. Verticle: 애플리케이션의 구성 요소로, 이벤트 루프에서 실행됩니다.
  2. Event Bus: Verticle 간의 메시지를 전송하는 시스템입니다.
  3. Future: 비동기 작업의 결과를 나타냅니다.

Vert.x 사용 예제

Maven 프로젝트의 pom.xml 파일에 Vert.x 의존성을 추가합니다.

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-core</artifactId>
  <version>4.3.3</version>
</dependency>

Verticle을 정의하고, 이벤트 버스를 통해 메시지를 처리하는 예제를 살펴보겠습니다.

import io.vertx.core.AbstractVerticle;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.EventBus;

public class VertxExample extends AbstractVerticle {
  public static void main(String[] args) {
    Vertx vertx = Vertx.vertx();
    vertx.deployVerticle(new VertxExample());
  }

  @Override
  public void start() {
    EventBus eventBus = vertx.eventBus();

    eventBus.consumer("greetings", message -> {
      System.out.println("Received message: " + message.body());
      message.reply("Hello from Vert.x!");
    });

    eventBus.request("greetings", "Hi there!", reply -> {
      if (reply.succeeded()) {
        System.out.println("Reply received: " + reply.result().body());
      }
    });
  }
}

이 예제에서는 Verticle을 정의하고, 이벤트 버스를 통해 메시지를 주고받습니다. eventBus.consumer 메서드를 통해 메시지를 수신하고, eventBus.request 메서드를 통해 메시지를 전송합니다.

Akka와 Vert.x 비교

  • Akka: 액터 모델을 기반으로 하여 메시지 기반의 동시성 문제를 해결합니다. 상태를 갖고 있는 객체인 액터를 사용하여 비동기 프로그래밍을 구현합니다. 분산 시스템에 적합합니다.
  • Vert.x: 이벤트 기반의 비동기 프레임워크로, 비동기 I/O 및 분산 애플리케이션에 강력합니다. 이벤트 루프와 이벤트 버스를 사용하여 비동기 작업을 처리합니다.

결론

프레임워크 레벨에서의 병렬 프로그래밍과 멀티스레딩은 고성능 애플리케이션을 구현하는 데 필수적입니다. Akka와 Vert.x는 각각의 장점을 가지고 있으며, 애플리케이션의 요구사항에 따라 적절한 프레임워크를 선택하여 사용할 수 있습니다. 이러한 프레임워크를 활용하여 복잡한 동시성 문제를 효율적으로 해결하고, 확장 가능하고 유지 보수하기 쉬운 애플리케이션을 개발할 수 있습니다.

Executor Framework, Fork/Join Framework

Executor Framework와 Fork/Join Framework는 자바에서 병렬 프로그래밍과 멀티스레딩을 효율적으로 관리하기 위한 두 가지 주요 프레임워크입니다. 이 두 프레임워크에 대해 깊이 있게 설명하고, 구체적인 코드 예제를 제공하겠습니다.

Executor Framework

Executor Framework는 자바 5에서 도입된 API로, 스레드를 직접 생성하고 관리하는 대신 스레드 풀을 통해 작업을 관리할 수 있도록 도와줍니다. 이는 작업을 제출하고, 스레드 풀에서 스레드를 할당받아 비동기적으로 실행되게 합니다.

주요 인터페이스와 클래스

  1. Executor: 작업을 실행하는 단순한 인터페이스입니다.
  2. ExecutorService: Executor를 확장한 인터페이스로, 스레드 풀을 관리하고, 작업의 제출과 종료를 처리합니다.
  3. ScheduledExecutorService: 일정한 시간 간격으로 작업을 실행할 수 있는 ExecutorService입니다.
  4. Executors: Executor와 ExecutorService를 생성할 수 있는 팩토리 클래스입니다.

Executor Framework 사용 예제

고정된 크기의 스레드 풀 생성

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task executed by " + threadName);
            });
        }

        executorService.shutdown();
    }
}

캐시된 스레드 풀 생성

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task executed by " + threadName);
            });
        }

        executorService.shutdown();
    }
}

단일 스레드 풀 생성

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 10; i++) {
            executorService.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Task executed by " + threadName);
            });
        }

        executorService.shutdown();
    }
}

Fork/Join Framework

Fork/Join Framework는 자바 7에서 도입된 API로, 대규모 작업을 작은 작업으로 분할하여 병렬로 처리하는 데 최적화되어 있습니다. 이는 주로 재귀적 알고리즘에 사용됩니다.

주요 클래스와 인터페이스

  1. ForkJoinPool: 작업을 병렬로 실행하기 위한 스레드 풀입니다.
  2. ForkJoinTask: ForkJoinPool에서 실행될 작업의 기본 클래스입니다.
  3. RecursiveTask: 결과를 반환하는 작업을 위한 ForkJoinTask의 서브클래스입니다.
  4. RecursiveAction: 결과를 반환하지 않는 작업을 위한 ForkJoinTask의 서브클래스입니다.

Fork/Join Framework 사용 예제

RecursiveTask 사용 예제

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class Fibonacci extends RecursiveTask<Integer> {
        final int n;

        Fibonacci(int n) {
            this.n = n;
        }

        @Override
        protected Integer compute() {
            if (n <= 1) return n;
            Fibonacci f1 = new Fibonacci(n - 1);
            Fibonacci f2 = new Fibonacci(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int result = pool.invoke(new Fibonacci(10));
        System.out.println("Fibonacci(10) = " + result);
    }
}

RecursiveAction 사용 예제

import java.util.concurrent.RecursiveAction;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinActionExample {
    static class PrintTask extends RecursiveAction {
        private final int start;
        private final int end;

        PrintTask(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        protected void compute() {
            if (end - start <= 5) {
                for (int i = start; i < end; i++) {
                    System.out.println(Thread.currentThread().getName() + " - " + i);
                }
            } else {
                int mid = (start + end) / 2;
                PrintTask task1 = new PrintTask(start, mid);
                PrintTask task2 = new PrintTask(mid, end);
                invokeAll(task1, task2);
            }
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        pool.invoke(new PrintTask(0, 20));
    }
}

결론

Executor Framework와 Fork/Join Framework는 자바에서 멀티스레딩과 병렬 프로그래밍을 관리하기 위한 강력한 도구들입니다. Executor Framework는 스레드 풀을 통해 작업을 효율적으로 관리하고, Fork/Join Framework는 큰 작업을 작은 작업으로 분할하여 병렬로 처리하는 데 최적화되어 있습니다. 이 두 프레임워크를 적절히 활용하여 고성능의 멀티스레드 애플리케이션을 개발할 수 있습니다.

Executor Framework은 쓰레드를 직접 관리하는 대신 쓰레드 풀을 관리함으로 작업을 돕네?

네, 맞습니다. Executor Framework는 쓰레드를 직접 관리하는 대신, 쓰레드 풀을 관리하여 작업을 효율적으로 처리할 수 있게 합니다. 이를 통해 자원의 낭비를 줄이고, 애플리케이션의 성능을 향상시킬 수 있습니다. Executor Framework의 주요 개념과 고급 기능들을 조금 더 깊이 있게 살펴보겠습니다.

Executor Framework의 주요 구성 요소

  1. Executor 인터페이스
  2. ExecutorService 인터페이스
  3. ScheduledExecutorService 인터페이스
  4. Executors 팩토리 클래스
  5. Future 인터페이스
  6. CompletionService 인터페이스

1. Executor 인터페이스

Executor 인터페이스는 단순한 작업 실행기를 정의합니다. 하나의 메서드 execute(Runnable command)를 통해 작업을 실행합니다.

Executor executor = new Executor() {
    @Override
    public void execute(Runnable command) {
        new Thread(command).start();
    }
};

executor.execute(() -> System.out.println("Task executed!"));

2. ExecutorService 인터페이스

ExecutorService는 Executor를 확장하며, 더 많은 제어 기능을 제공합니다. 주로 다음 메서드들을 사용합니다:

  • submit(): 작업을 제출하고, 작업의 결과를 Future 객체로 반환합니다.
  • shutdown(): 더 이상의 작업 제출을 막고, 이미 제출된 작업이 완료될 때까지 기다립니다.
  • shutdownNow(): 현재 실행 중인 작업을 중단하고, 대기 중인 작업을 취소합니다.
  • awaitTermination(): 모든 작업이 완료되거나 타임아웃이 발생할 때까지 대기합니다.
ExecutorService executorService = Executors.newFixedThreadPool(3);

for (int i = 0; i < 10; i++) {
    executorService.submit(() -> {
        String threadName = Thread.currentThread().getName();
        System.out.println("Task executed by " + threadName);
    });
}

executorService.shutdown();
try {
    if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
        executorService.shutdownNow();
    }
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

3. ScheduledExecutorService 인터페이스

ScheduledExecutorService는 특정 시간 간격으로 작업을 실행할 수 있게 합니다. 주로 다음 메서드들을 사용합니다:

  • schedule(): 일정 시간이 지난 후에 작업을 실행합니다.
  • scheduleAtFixedRate(): 일정한 시간 간격으로 작업을 반복 실행합니다.
  • scheduleWithFixedDelay(): 이전 작업이 종료된 후 일정 시간 간격으로 작업을 반복 실행합니다.
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Scheduled task executed at " + System.nanoTime());
scheduledExecutorService.schedule(task, 5, TimeUnit.SECONDS);

scheduledExecutorService.scheduleAtFixedRate(task, 1, 3, TimeUnit.SECONDS);
scheduledExecutorService.scheduleWithFixedDelay(task, 1, 3, TimeUnit.SECONDS);

4. Executors 팩토리 클래스

Executors 클래스는 다양한 ExecutorService 인스턴스를 생성할 수 있는 팩토리 메서드를 제공합니다:

  • newFixedThreadPool(): 고정된 크기의 스레드 풀을 생성합니다.
  • newCachedThreadPool(): 필요에 따라 스레드를 생성하고, 재사용 가능한 스레드가 없으면 새로운 스레드를 생성합니다.
  • newSingleThreadExecutor(): 단일 스레드로 작업을 처리합니다.
  • newScheduledThreadPool(): 일정 시간 간격으로 작업을 실행할 수 있는 스레드 풀을 생성합니다.

5. Future 인터페이스

Future는 비동기 작업의 결과를 나타내며, 다음과 같은 메서드를 제공합니다:

  • get(): 작업이 완료될 때까지 대기하고, 결과를 반환합니다.
  • get(long timeout, TimeUnit unit): 주어진 시간 동안 대기하고, 결과를 반환합니다.
  • cancel(boolean mayInterruptIfRunning): 작업을 취소합니다.
  • isCancelled(): 작업이 취소되었는지 확인합니다.
  • isDone(): 작업이 완료되었는지 확인합니다.
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<Integer> future = executorService.submit(() -> {
    Thread.sleep(2000);
    return 123;
});

try {
    Integer result = future.get(1, TimeUnit.SECONDS); // 1초 동안 대기
    System.out.println("Result: " + result);
} catch (TimeoutException e) {
    System.out.println("Timeout occurred");
    future.cancel(true);
} finally {
    executorService.shutdown();
}

6. CompletionService 인터페이스

CompletionService는 제출된 작업의 완료 결과를 관리합니다. 주로 ExecutorCompletionService 클래스를 통해 사용됩니다. 이는 작업이 완료된 순서대로 결과를 반환받을 수 있게 합니다.

ExecutorService executorService = Executors.newFixedThreadPool(3);
CompletionService<Integer> completionService = new ExecutorCompletionService<>(executorService);

for (int i = 0; i < 5; i++) {
    final int taskId = i;
    completionService.submit(() -> {
        Thread.sleep((taskId + 1) * 1000);
        return taskId;
    });
}

for (int i = 0; i < 5; i++) {
    Future<Integer> future = completionService.take(); // 완료된 작업의 Future를 가져옴
    try {
        Integer result = future.get();
        System.out.println("Task completed with result: " + result);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

executorService.shutdown();

결론

Executor Framework는 멀티스레딩과 비동기 프로그래밍을 보다 쉽게 관리할 수 있도록 해줍니다. 다양한 스레드 풀 구현을 제공하여 자원을 효율적으로 사용할 수 있게 하며, 작업의 제출, 실행, 결과 처리 등을 체계적으로 관리할 수 있습니다. Future와 CompletionService를 통해 비동기 작업의 결과를 관리할 수 있으며, ScheduledExecutorService를 통해 일정 간격으로 작업을 실행할 수 있습니다. 이러한 기능

들은 자바 애플리케이션에서 멀티스레딩을 보다 효율적이고 안정적으로 구현하는 데 큰 도움을 줍니다. 다음으로 Fork/Join Framework에 대해 더 깊이 있게 알아보겠습니다.

Fork/Join Framework

Fork/Join Framework는 자바 7에서 도입된 API로, 대규모 작업을 작은 작업으로 분할하여 병렬로 처리하는 데 최적화되어 있습니다. 이는 주로 재귀적 알고리즘이나 대규모 데이터 처리에 사용됩니다.

주요 구성 요소

  1. ForkJoinPool: 작업을 병렬로 실행하기 위한 스레드 풀입니다.
  2. ForkJoinTask: ForkJoinPool에서 실행될 작업의 기본 클래스입니다.
  3. RecursiveTask: 결과를 반환하는 작업을 위한 ForkJoinTask의 서브클래스입니다.
  4. RecursiveAction: 결과를 반환하지 않는 작업을 위한 ForkJoinTask의 서브클래스입니다.

ForkJoinPool

ForkJoinPool은 작업을 병렬로 실행하기 위한 스레드 풀입니다. 이는 ForkJoinTask를 실행하기 위해 설계되었습니다.

ForkJoinPool pool = new ForkJoinPool();

ForkJoinTask

ForkJoinTask는 ForkJoinPool에서 실행될 작업의 기본 클래스입니다. 이는 RecursiveTask와 RecursiveAction으로 나눌 수 있습니다.

  • RecursiveTask<V>: 결과를 반환하는 작업
  • RecursiveAction: 결과를 반환하지 않는 작업

RecursiveTask 사용 예제

RecursiveTask는 결과를 반환하는 작업을 정의할 때 사용됩니다. 예를 들어, 피보나치 수열을 계산하는 작업을 정의할 수 있습니다.

import java.util.concurrent.RecursiveTask;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinExample {
    static class Fibonacci extends RecursiveTask<Integer> {
        final int n;

        Fibonacci(int n) {
            this.n = n;
        }

        @Override
        protected Integer compute() {
            if (n <= 1) return n;
            Fibonacci f1 = new Fibonacci(n - 1);
            Fibonacci f2 = new Fibonacci(n - 2);
            f1.fork();
            return f2.compute() + f1.join();
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int result = pool.invoke(new Fibonacci(10));
        System.out.println("Fibonacci(10) = " + result);
    }
}

RecursiveAction 사용 예제

RecursiveAction은 결과를 반환하지 않는 작업을 정의할 때 사용됩니다. 예를 들어, 배열을 분할하여 각 요소를 출력하는 작업을 정의할 수 있습니다.

import java.util.concurrent.RecursiveAction;
import java.util.concurrent.ForkJoinPool;

public class ForkJoinActionExample {
    static class PrintTask extends RecursiveAction {
        private final int start;
        private final int end;
        private final int[] array;

        PrintTask(int[] array, int start, int end) {
            this.start = start;
            this.end = end;
            this.array = array;
        }

        @Override
        protected void compute() {
            if (end - start <= 5) {
                for (int i = start; i < end; i++) {
                    System.out.println(Thread.currentThread().getName() + " - " + array[i]);
                }
            } else {
                int mid = (start + end) / 2;
                PrintTask task1 = new PrintTask(array, start, mid);
                PrintTask task2 = new PrintTask(array, mid, end);
                invokeAll(task1, task2);
            }
        }
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        int[] array = new int[20];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        pool.invoke(new PrintTask(array, 0, array.length));
    }
}

결론

Fork/Join Framework는 대규모 작업을 작은 작업으로 분할하여 병렬로 처리하는 데 최적화된 프레임워크입니다. 이는 특히 재귀적 알고리즘이나 대규모 데이터 처리에 유용합니다. ForkJoinPool을 통해 작업을 병렬로 실행하며, RecursiveTask와 RecursiveAction을 통해 각각 결과를 반환하는 작업과 결과를 반환하지 않는 작업을 정의할 수 있습니다.

Executor Framework와 Fork/Join Framework를 적절히 활용하면 자바 애플리케이션에서 복잡한 멀티스레딩 문제를 효율적으로 해결하고, 애플리케이션의 성능을 극대화할 수 있습니다.

Executor Framework은 스프링 프레임워크 내부에서 같이 설치하고 동작할 수 있어?

네, Executor Framework는 스프링 프레임워크와 함께 사용할 수 있습니다. 스프링은 다양한 방식으로 멀티스레딩을 지원하며, Executor Framework와 통합하여 보다 효율적인 스레드 관리를 할 수 있습니다. 이를 통해 비동기 작업을 처리하거나 스레드 풀을 구성하는 작업을 보다 쉽게 할 수 있습니다.

Spring에서 Executor Framework 사용하기

스프링 프레임워크에서 Executor Framework를 사용하는 방법을 단계별로 설명하겠습니다.

1. Spring Configuration 클래스 설정

먼저, 스프링의 설정 클래스에서 Executor 빈을 정의합니다. ThreadPoolTaskExecutor를 사용하여 스레드 풀을 구성할 수 있습니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
public class ThreadPoolConfig {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("MyExecutor-");
        executor.initialize();
        return executor;
    }
}

2. @Async 애노테이션 사용

스프링의 @Async 애노테이션을 사용하여 비동기 메서드를 정의할 수 있습니다. @EnableAsync 애노테이션을 통해 비동기 처리를 활성화해야 합니다.

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Service;

@Service
@EnableAsync
public class AsyncService {

    @Async("taskExecutor")
    public void executeAsyncTask() {
        System.out.println("Execute method asynchronously - " + Thread.currentThread().getName());
    }
}

3. Spring Boot Application 클래스 설정

스프링 부트 애플리케이션에서 비동기 처리를 활성화하기 위해 @EnableAsync 애노테이션을 추가합니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringAsyncApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringAsyncApplication.class, args);
    }
}

4. 비동기 메서드 호출

서비스 클래스에서 비동기 메서드를 호출합니다.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

    @Autowired
    private AsyncService asyncService;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("Calling async method from main thread - " + Thread.currentThread().getName());
        asyncService.executeAsyncTask();
    }
}

예제 프로젝트 구성

전체적인 프로젝트 구조는 다음과 같습니다:

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           ├── SpringAsyncApplication.java
│   │           ├── AppRunner.java
│   │           ├── AsyncService.java
│   │           └── ThreadPoolConfig.java
│   └── resources/
│       └── application.properties
└── test/
    └── java/

결론

이와 같이 스프링 프레임워크에서 Executor Framework를 활용하여 비동기 작업을 처리할 수 있습니다. @Async 애노테이션과 ThreadPoolTaskExecutor를 사용하여 비동기 메서드를 정의하고, 스레드 풀을 효율적으로 관리할 수 있습니다. 이를 통해 애플리케이션의 성능을 최적화하고, 비동기 작업을 간편하게 처리할 수 있습니다.