본문 바로가기

Java 공부

12장 스레드풀(1)

스레드 폭증

-병렬 작업 처리가 많아지면 스레드의 개수가 증가한다.

-스레드 생성과 스케쥴링으로 인해 CPU가 바빠지고, 메모리 사용량이 늘어난다.

-따라서 애플리케이션의 성능이 급격히 저하된다.


다양한 작업을 동시에 처리하면서도 많은 수의 작업에 따른 무분별한 스레드의 생성을 막고, 

애플리케이션의 성능이 저하되지 않게 하기 위해서 스레드를 효율적으로 관리하는 스레드 풀이 등장하였다.



스레드 풀(Thread Pool)

-스레드 풀은 스레드의 개수를 미리 정해놓고 그 한도 내에서 사용하는 것을 말한다.

-작업 처리에 사용되는 스레드를 제한된 개수만큼 미리 생성 

-작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다.

-작업 처리가 끝난 스레드는 작업 결과를 애플리케이션으로 전달한다.

-스레드는 다시 작업큐에서 새로운 작업을 가져와 처리한다.


ExecutorService(스레드 풀)의 동작 원리

-작업 큐: 작업을 저장하는 공간

-작업 큐 공간에 있는 작업들을 가지고 와서 처리하는 스레드들이 제한된 개수 만큼 있다.

-작업 큐 안에 있는 작업 하나를 스레드 하나가 가져와서 처리한다. 작업 처리가 다 된 후에는 다른 작업을 가지고 와서 처리한다.

-실행 원리

 1. Runnable객체를 생성하여 스레드 풀에 작업처리 요청

 2. 작업 큐에 Runnable객체 저장

 3. 스레드가 작업 큐에 있는 작업 객체(Runnable객체)를 하나씩 처리

 4. 스레드가 처리하고 난 결과는 애플리케이션에게 전달한다.

-따라서 아무리 많은 작업이 있다고 해도, 작업 큐 안에 있는 작업의 수만 늘어날 뿐 스레드의 개수는 한정되어져 있기 때문에 증가하지 않는다.

 이로 인해 애플리케이션의 부하를 가져오지 않는다.

-서버 프로그램을 만들 때 스레드 풀이 자주 사용된다.


ExecutorService 인터페이스와 Executors 클래스

-스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 인터페이스와 클래스를 제공한다.

-Executors의 정적 메소드를 이용해서 ExecutorService구현 객체 생성

-스레드 풀 = ExecutorService 객체


스레드풀 생성

스레드풀 생성 방법에는 2가지의 방법이 있다.

1) ExecutorService의 정적 메소드를 사용하여 스레드 풀 생성

   ExecutorService의 두 가지의 정적 메소드 중 하나로 간편하게 스레드 풀을 생성 할 수 있다.

메소드명(매개변수) 

 초기 스레드수

코어 스레드수 

최대 스레드수 

 newCachedThreadPool()

0 

0 

Integer.MAX_VALUE 

 newFixedThreadPool(int nThreads)

0

nThreads 

nThreads 

*초기 스레드 수 : 스레드 풀을 처음 만들 때 기본적으로 들어가 있는 스레드 풀 

*코어 스레드 수:  스레드 풀에 스레드가 증가되어 많은 수의 스레드가 있을 경우 사용되지 않는 스레드를 제거할 때, 

최소한으로 유지해야하는 스레드의 수

*최대 스레드 수: 생성할 수 있는 최대의 스레드 수


-newCachedThreadPool()

-int값이 가질 수 있는 최대 값만큼 스레드가 추가되거나, 운영체제의 상황에 따라 달라진다.

-1개 이상의 스레드가 추가되었을 경우

-60초 동안 추가된 스레드가 아무 작업을 하지 않으면 

-추가된 스레드를 종료하고 풀에서 제거한다.

ExecutorService executorService = Executors. newCachedThreadPool();


-newFixedThreadPool(int nThreads)

-코어 스레드 개수와 최대 스레드 개수가 매개값으로 준 nThread이다.

-스레드가 작업을 처리하지 않고 놀고 있더라도 스레드 개수가 줄 지 않는다.

ExecutorService executorService = Excutors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

* Runtime.getRuntime().availableProcessors():

   현재 CPU의 코어의 수를 얻게 된다. ex)더블 코어일 경우 :2 , 코드 코어일 경우: 4


2) ThreadPoolExecutor을 이용한 직접 생성

- newCachedThreadPool()과 newFixedThreadPool(int nThreads)가 내부적으로 생성

-스레드의 수를 자동으로 관리하고 싶을 경우 직접 생성해서 사용

ex) 

-코어 스레드 개수가 3, 최대 스레드 개수가 100인 스레드 풀을 생성

-3개를 제외한 나머지 추가된 스레드가 120초 동안 놀고 있을 경우

-해당 스레드를 제거해서 스레드 수를 관리

ExecutorService threadPool = new ThreadPoolExecutor(

3,                 //코어 스레드 개수

100,              //최대 스레드 개수

120L,              //놀고 있는 시간

TimeUnit.SECONDS       //놀고 있는 단위-->SECONDS이므로 초

new SynchronousQueue<Runnable>()      //작업큐

);


스레드풀 종료

-스레드풀의 스레드는 기본적으로 데몬 스레드가 아니다.

-main스레드가 종료되더라도 스레드풀의 스레드는 작업을 처리하기 위해 계속 실행되므로

-애플리케이션은 종료되지 않는다.

-따라서 스레드풀을 종료해서 모든 스레드를 종료시켜야 한다.

-스레드풀 종료 메소드

리턴타입 

메소드명(매개변수) 

설명 

 void

shutdown() 

 현재 처리중인 작업뿐만 아니라 작업큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드 풀을 종료시킨다.

 List<Runnable>

shutdownNow() 

 현재 작업 처리 중인 스레드를 interrupt해서 작업 중지를 시도하고 스레드풀을 종료시킨다. 리턴값은 작업큐에 있는 미처리된 작업(Runnable)의 목록이다.

가급적이면 shutdwonNow()를 호출하지 않는것이 좋다.

why? 작업이 불완전하게 끝날 수 있기 때문에.

 boolean

 awaitTermination(

long timeout,

TimeUnit unit)

 shutdown() 메소드 호출 이후, 모든 작업 처리를 timeout시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리 중인 스레드를 interrupt(종료)하고 false를 리턴한다.



다음은 스레드 풀에 작업 처리를 요청할 때, 작업을 정의한 Runnable 객체를 생성하는 방법에 대해 알아보자.


작업 생성

-작업을 생성하는 방법에는 두가지가 있다. Runnable 객체로 생성 또는 Callable 객체로 생성

-Runnable과 Callable의 차이점?

-작업 처리 완료 후 리턴값이 있는냐 없느냐이다.

 리턴값이 없다면 Runnable, 리턴값이 있다면 Callable

Runnable 구현 클래스 

 Callable 구현 클래스

 Runnable task = new Runnable() {

        @Override

        public void run() {

                 //스레드가 처리할 작업 내용

           }

   }

 Callable<T> task = new Callable<T> {

            @Override

            public T call() throws Exception {

                     //스레드가 처리할 작업 내용

                     return T;

                   }

        }

* 리턴값이 String 이라면 T대신 String을, Int라면 T대신 int를 넣어주면 된다.

-스레드풀에서 작업 처리

     -작업 큐에서 Runnable 또는 Callable 객체를 가져와

-스레드로 하여금 run()과 call()메소드를 실행토록 하는 것이다.



작업의 내용이 담겨져있는 Runnable 또는 Callable객체를 만들었다면 이 작업을 스레드풀에 처리해주길 요청하는 법에 대해 알아보자.



작업 처리 요청

-ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

-작업 처리 요청을 위해 ExecutorService는 다음 두가지 종류의 메소드를 제공한다.


    *작업 결과를 얻고 싶다면 submit을 이용하고, 작업 결과를 얻을 필요가 없다면 execute를 이용하면 된다.

-작업 처리 도중 예외가 발생할 경우

-execute()

스레드가 종료되고 해당 스레드는 제거된다.

따라서 스레드풀은 다른 작업 처리를 위해 새로운 스레드를 생성한다.

-submit()

스레드가 종료되지 않고 다음 작업을 위해 재사용된다.


일반적으로 스레드는 많이 생성할 수록 CPU에 메모리를 할당하는 것이기 때문에 시스템에 효율적이지 못하다.

따라서 스레드는 가능한 재사용할 수 있으면 재사용하는 것이 좋은데

그런 맥락에서, submit()메소드는 작업객체를 스레드에게 넘겨줄 때 작업 처리 도중  예외가 발생한 스레드를 

종료하지 않고, 그대로 뒀다가 그다음 작업을 위해 재사용된다.

이러한 측면에서 submit()은 execute()보다 효율적이다라고 할 수 있으며 그렇기 때문에 리턴값이 없는 Runnable객체도 매개값으로 하여

작업을 처리하도록 요청할 수 있는 것이다.



본 포스팅은 이것이 자바다 책을 참고하여 작성하였습니다.

'Java 공부' 카테고리의 다른 글

12장 스레드풀(3)  (0) 2018.12.11
12장 스레드풀(2)  (0) 2018.12.11
12장 스레드 그룹  (0) 2018.12.09
12장 데몬 스레드  (0) 2018.12.08
12장 스레드 상태 제어(2)  (0) 2018.12.07