이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/thread-pools.html




쓰레드 풀은 동시에 가동하는 쓰레드 수의 최대치에 제한을 둘 때 유용하다. 새 쓰레드를 생성하는 일은 성능에 있어 오버헤드가 따른다. 그리고 각 쓰레드에는 각각의 스택을 위한 메모리가 할당되어야 한다. 


작업을 실행할 때마다 새 쓰레드를 생성하여 시작하는 일 대신, 쓰레드 풀에 작업을 넘기는 방법으로 작업을 실행할 수 있다. 쓰레드 풀은 놀고 있는 쓰레드가 생기면 즉시 작업을 쓰레드에 할당하여 실행되도록 한다. 작업은 내부적으로 블로킹 큐에 저장되며, 쓰레드 풀의 쓰레드들은 여기서 작업을 빼서 실행한다. 큐에 새로운 작업이 들어오면 놀고 있는 쓰레드가 큐에서 작업을 빼서 실행한다. 이를 두고 쓰레드에 작업이 할당되었다고 하며, 작업이 할당되지 않은 쓰레드들은 큐에 새 작업이 들어올 때까지 대기 상태로 유지된다.


스레드 풀은 멀티쓰레드 서버에 주로 사용된다. 네트워크를 통해 서버로 도착하는 커넥션들은 각각 하나의 작업으로 포장되어 쓰레드 풀에 넘겨진다. 쓰레드들은 이렇게 넘겨받은 커넥션들의 요청을 동시에 처리한다. 이에 대해서는 나중에 자바로 멀티쓰레드 서버를 구현하는 방법에 대해 자세히 살펴볼 것이다. 


Java 5 는 java.util.concurrent 패키지에 쓰레드 풀을 포함한다. 이 쓰레드 풀 클래스에 대한 정보는 java.util.concurrent.ExecutorService(원문) 에서 읽어볼 수 있다. 쓰레드 풀을 직접 구현할 필요는 없지만, 여기서도 역시 쓰레드 풀 구현에 대해 자세히 알아보는 것은 도움이 되리라 본다.


아래는 간단한 쓰레드 풀 구현이다. 여기서의 BlockingQueue 블로킹 큐 튜토리얼에서 등장한 구현체를 사용했음을 알아두기 바란다. 실제로 쓰레드 풀을 구현할 때는 자바의 빌트인 블로킹 큐를 사용하도록 한다.


public class ThreadPool {

    private BlockingQueue taskQueue = null;
    private List<PoolThread> threads = new ArrayList<PoolThread>();
    private boolean isStopped = false;

    public ThreadPool(int noOfThreads, int maxNoOfTasks){
        taskQueue = new BlockingQueue(maxNoOfTasks);

        for(int i=0; i<noOfThreads; i++){
            threads.add(new PoolThread(taskQueue));
        }
        for(PoolThread thread : threads){
            thread.start();
        }
    }

    public synchronized void  execute(Runnable task) throws Exception{
        if(this.isStopped) throw
            new IllegalStateException("ThreadPool is stopped");

        this.taskQueue.enqueue(task);
    }

    public synchronized void stop(){
        this.isStopped = true;
        for(PoolThread thread : threads){
           thread.doStop();
        }
    }

}
public class PoolThread extends Thread {

    private BlockingQueue taskQueue = null;
    private boolean       isStopped = false;

    public PoolThread(BlockingQueue queue){
        taskQueue = queue;
    }

    public void run(){
        while(!isStopped()){
            try{
                Runnable runnable = (Runnable) taskQueue.dequeue();
                runnable.run();
            } catch(Exception e){
                //log or otherwise report exception,
                //but keep pool thread alive.
            }
        }
    }

    public synchronized void doStop(){
        isStopped = true;
        this.interrupt(); //break pool thread out of dequeue() call.
    }

    public synchronized boolean isStopped(){
        return isStopped;
    }
}


쓰레드 풀 구현은 두 파트로 나뉜다. ThreadPool 클래스는 쓰레드 풀 사용을 위한 공용 인터페이스이다. PoolThread 클래스는 작업을 실행하기 위한 쓰레드를 구현한다.


작업을 실행하기 위해서는 ThreadPool.execute(Runnable r) 이 호출된다. 이 메소드는 Runnable 구현체를 파라미터로 받는다. 이 Runnable 은 내부의 블로킹 큐에 삽입되고, 작업 쓰레드가 가져가기를 기다린다.


Runnable 은 작업이 할당되지 않는 PoolThread 에 의해 큐에서 빠져나와 실행된다. 이 과정은 PoolThread.run() 메소드에서 볼 수 있다. 실행이 완료되면 PoolThread 는 루프를 돌며 큐에 실행할 작업이 있는지 계속 확인한다. 이 루프는 사용자에 의해 쓰레드 풀이 정지될 때까지 지속된다.


ThreadPool 을 정지하기 위해서는 ThreadPool.stop() 이 호출된다. 이 메소드는 풀 내부의 isStopped 변수에 쓰레드 풀이 정지되었음을 표시한다. 그리고 작업 쓰레드 각각에 doStop() 을 호출하여 더이상 새 작업을 실행하지 않도록 한다. 풀이 정지된 다음 execute() 메소드가 호출되면 IllegalStateException 을 던져 작업이 실행될 수 없음을 알린다.


작업 쓰레드들은 자신이 현재 실행중인 작업이 완료된 다음 정지된다. PoolThread.doStop() 메소드를 보면 this.interrupt() 호출이 있다. 이 호출은 taskQueue.dequeue() 호출 대기 상태로 들어간 쓰레드의 대기 상태를 풀어버리고 InterruptedException 과 함께 dequeue() 호출을 빠져나가도록 한다. 이 예외는 PoolThread.run() 에서 캐치된다. 그리고 while 루프의 조건문에서 isStopped 의 값을 확인한다. 쓰레드 풀이 정지되어 isStopped 는 이제 true 이다. PoolThread.run() 은 종료되고 쓰레드는 죽게 된다.




'Java > Concurrency' 카테고리의 다른 글

동기화 장치 해부(Anatomy of a Synchronizer)  (0) 2017.05.25
컴페어 스왑(Compare and Swap)  (0) 2017.05.24
블로킹 큐(Blocking Queues)  (3) 2017.05.22
세마포어(Semophores)  (0) 2017.05.22
재진입 락아웃(Reentrance Lockout)  (0) 2017.05.21

+ Recent posts