ExecutorService

Seung jun Cha·2023년 7월 16일
0
post-thumbnail

1. 개념

  • 병렬 작업 시 여러 개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA 라이브러리이다. ExecutorService에 Task만 지정해주면 친절하게 알아서 ThreadPool을 이용해서 Task를 실행하고 관리한다.

  • 쓰레드는 Queue로 관리된다.
    ThreadPool에 있는 Thread수보다 Task가 많으면, 미실행된 Task는 Queue에 저장되고, 실행을 마친 Thread로 할당되어 순차적으로 수행된다.

2. 사용방법

  1. ThreadPoolExecutor로 객체 생성하기
  • ExecutorService는 인터페이스이기 때문에 구현체인 ThreadPoolExecutor로 초기화할 수 있다.
ExecutorService executorService = new ThreadPoolExecutor(int corePoolSize, 
                                int maximumPoolSize, 
                                long keepAliveTime, 
                                TimeUnit unit, 
                                BlockingQueue<Runnable> workQueue);

ExecutorService executorService = new ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler);

ExecutorService executorService = new ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);
  1. Executors 클래스에서 제공하는 Static Factory Method 사용하기
ExecutorService executorService = Executors.newCachedThreadPool();

ExecutorService executorService = Executors.newFixedThreadPool(int nThreads);

ExecutorService executorService = Executors.newSingleThreadExecutor();
  • CachedThreadPool
    쓰레드를 캐싱하는 쓰레드풀 (일정시간동안 쓰레드를 검사하여 60초동안 작업이 없으면 Pool에서 제거한다.)
    CachedThreadPool은 쓰레드수가 폭발적으로 증가할 수 있다는 단점이 있다.
    Thread의 제한 없이 무한정 생성하고, 해당 쓰레드의 작업이 60초간 없을 경우 Pool에서 제거하는 방식이기 때문에 작업이 계속적으로 쌓이는 환경에서는 해당 Thread가 소멸되는 것보다, 생성되는 양이 더 많을 것

  • FixedThreadPool
    고정된 개수를 가진 쓰레드풀
    fixedThreadPool을 생성할때, 해당 머신의 CPU코어수를 기준으로 생성하면 더 좋은 퍼포먼스를 얻을 수 있다.

  • SingleThreadExecutor
    한 개의 쓰레드로 작업을 처리하는 쓰레드풀
    싱글 쓰레드의 작업을 처리할때 고려해야 할 race-condition이라던지 하는 부분들을 알아서 처리

3. 제공되는 메서드

ExecutorService에 작업을 submit하면, 내부에서 해당 작업을 스케쥴링 하면서 적절하게 일을 처리한다.

ThreadPool에 있는 쓰레드들이 각자 본인의 Task를 가지고 작업을 처리하여, 개발자 입장에서는 쓰레드들의 생명주기를 따로 관리할 필요가 없다.

작업을 할당하기 위해 제공되는 메서드들

  • execute()
    리턴 타입이 void로 Task의 실행 결과나 Task의 상태를 알 수 없다.
  • submit()
    Task를 할당하고 Future 타입의 결과값을 받는다. 결과 리턴이 되어야해서 Callable을 구현한 Task를 인자로 준다.
  • invokeAny()
    Task를 Collection에 넣어서 인자로 넘겨준다. 실행에 성공한 Task 중 하나의 리턴값을 반환한다.
  • invokeAll()
    Task를 Collection에 넣어서 인자로 넘겨줄 수 있다. 모든 Task의 리턴값을 List<Future<>>로 반환한다.
  • shutdown()
    실행 중인 모든 Task가 수행되면 종료
  • shutdownNow()
    실행중인 Thread들을 즉시 종료. 하지만 모든 Thread가 동시에 종료되는 것을 보장하지는 않고 실행되지 않은 Task를 반환
    두 개의 shutdown 메서드가 결합된 awaitTermination()을 사용하는 것이 추천된다.

이 메서드는 먼저 새로운 Task가 실행되는 것을 막고, 일정 시간동안 실행 중인 Task가 완료되기를 기다린다. 만일 일정 시간동안 처리되지 않은 Task에 대해서는 강제로 종료시킨다.

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

4. 동기화 문제

  • 아래의 그림은 두개의 스레드가 동시에 자원에 접근하는 상황이다. 스레드 1이 데이터를 수정하여 갱신하기 전에 스레드 2가 접근하여 값을 수정하는 상황이다. 이렇게 되면 데이터에 오류가 생기게 된다.

4-2 해결방법

  1. synchronized : 메서드에 synchronized를 사용하면 해당 메서드에는 한 개의 스레드만 접근이 가능해진다. 하지만 synchronized는 하나의 프로세스 안에서만 보장이 된다. 일반적으로 서버 1개당 1개의 프로세스로 이루어진다. 실무에서는 보통 2대 이상의 서버를 사용하므로 synchronized가 사용되는 일은 거의 없다. synchronized는 인스턴스단위로 thread-safe 이 보장이 되고, 여러서버가 된다면 여러개의 인스턴스가 있는것과 동일하기 때문에 서버가 2개 이상이라면 synchronized를 사용하지 않은 것과 동일한 상황이 발생한다.
  2. Pessimistic Lock : 실제 데이터에 Lock을 거는 방법
  3. optimistic Lock :직접 Lock를 거는 것이 아닌 version 정보를 이용하는 방법
update set version + 1 , quantity = 2 
from stock 
where id =1 and version 1 
  1. Named Lock

-참고
https://simyeju.tistory.com/119

0개의 댓글