Thread in Java

쓰레드란 프로그램 실행의 가장 작은 단위이다. 일반적으로는 Main 하나로 동작시키겠지만 웹 서버 등에서는 계속해서 클라이언트의 연결 요청을 받아줘야하기에 매 요청에 대해서 동시성을 지원해줘야한다.

Java 버전에 따른 스레드의 발전 과정 등에 대해서 알아보자.

Java 버전 Thread
Java5 이전 Runnable, Thread
Java6 Callable, Future 및 Executor, ExecutorService, Executors
Java7 Fork/Join 및 RecursiveTask
Java8 CompletableFuture
Java9 Flow

Runnable

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 은 추상 메서드 run 을 가지는 인터페이스이다.

단 하나의 메서드를 가지기에 람다로 구현할 수 있으며 해당 내용이 곧 스레드가 실제 실행할 내용이다.

실제 웹 서버의 코드에서도 Thread 에 Runnable 인터페이스를 사용하여 간단하게 스레드를 구성하였다.

Thread

public class Thread implements Runnable {
    ...
}

Thread 는 클래스이다.

해당 클레스에서는 스레드를 멈추거나 Exception 을 발생시키고, 다른 스레드와의 작업 순서를 제어하는 등 직접적인 제어가 가능한 메서드들을 가진다.

Thread 는 말 그대로 클래스이기에 사용하기 위해서는 해당 클래스를 상속받아 run 메서드를 override 해야한다.

클래스이기에 많은 자원을 사용하고 상속 클래스를 구현해야하는 등 Runnable 보다도 더 많은 자원이 필요하기에 대부분 Runnable 을 사용한다.

결국 Thread, Runnable 둘 다 저수준 API 에 의존하며, 결과값을 반환하지 못하고, 스레드 시작과 종료에 대한 오버헤드 및 관리에 대한 어려움 문제를 가지고 있다.

ThreadPool

image

ThreadPool 은 매번 스레드를 시작 및 종료하는 것이 아니라 미리 스레드를 만들어놓고 필요에 따라 작업을 할당한다.

작업이 들어오면 Blocking Queue 를 통해 작업이 쌓이게 되고 순서대로 빈 스레드에 할당되어 작업을 실행한다.

        try (ServerSocket listenSocket = new ServerSocket(port)) {
            logger.info("Web Application Server started {} port.", port);

            // 클라이언트가 연결될때까지 대기한다.
            Socket connection;
            ExecutorService executor = Executors.newFixedThreadPool(10);

            while ((connection = listenSocket.accept()) != null) {
                executor.submit(new RequestHandler(connection));
//                Thread thread = new Thread(new RequestHandler(connection));
//                thread.start();
            }
        }

스레드를 사용해 커넥션을 처리하는 과정에서도 10 개의 스레드풀을 미리 생성해 계속해서 재사용할 수 있다.

newCachedThreadPool() 을 사용해 필요한 만큼의 스레드풀을 생성할 수도 있다.

Executors.newFixedThreadPool();

ExecutorService executor = Executors.newFixedThreadPool(30);

image

newFixedThreadPool() 은 실제 미리 스레드를 모두 만들어놓는다. 때문에 계속해서 요청을 보내게되면 1~30 번까지의 스레드를 모두 이용하고 다시 1번 스레드부터 순서대로 사용하게 된다.

Executors.newCachedThreadPool();

ExecutorService executor = Executors.newCachedThreadPool();

image

반면에 newCachedThreadPool() 같은 경우에는 미리 스레드를 만들어놓지 않고 실제 사용되는 만큼의 스레드만 생성한다. 때문에 실제 결과에서도 1~10 번까지의 스레드들이 재사용되는 것을 볼 수 있다.

Callable, Future

기존 Runnable 인터페이스는 결과를 반환할 수 없다는 문제가 있다.

따라서 이후 결과를 받을 수 있는 Callable 이 추가되었다.

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

함께 비동기 작업에 대한 결과 반환을 위한 Future 도 추가되었다.

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

해당 인터페이스는 작업의 상태를 파악하거나 get() 메서드를 통해서 실제 결과값을 반환받을 수 있다.

하지만 아직도 결과를 얻기 위해서는 Blocking 방식으로 대기해야한다는 단점이 있다. 이를 해결하기 위해 CompletableFuture 가 추가되었다.

CompletableFuture

기본적으로 Future 을 기반으로 외부에서 완료시킬 수 있기에 CompletableFuture 이다. 즉, 몇 초 이내로 응답이 안오면 기본값을 반환시키는 등의 작업이 가능해졌다. 뿐만 아니라 콜백 등록 및 Future 조합도 가능하기에 다방면으로 활용할 수 있다.

참고

[Java] Thread와 Runnable에 대한 이해 및 사용법

업데이트:

댓글남기기