[JAVA] ExecutorService 관련 공부
Development/Java

[JAVA] ExecutorService 관련 공부

반응형

ExecutorService 관련 공부


1. ExecutorService에 관하여

ExecutorService는 병렬작업 시 여러개의 작업을 효율적으로 처리하기 위해 제공되는 JAVA의 라이브러리이다.

통상적으로 작업을 분리하고 수행하는 작업을 손수 구현하고자 하면.. 각기 다른 Thread를 생성해서 작업을 처리하고, 처리가 완료되면 해당 Thread를 제거하는 작업을 진행해야 한다.

이러한 일련의 작업을 이 클래스를 이용해서 쉽게 처리가 가능하다.

흔히 말하는 ThreadPool을 구현하기가 매우 용이하므로, Java에서 쓰레드풀을 생성해서 사용하고자 하면 (손수 구축이 아닌.. 보장된 동작을 원한다면 이를 사용하길 적극 추천) 반드시 사용을 고려해봐야 할 유틸리티성 클래스이다.

ExecutorService에는 다음과 같은 종류가 존재한다.

  • CachedThreadPool : 쓰레드를 캐싱하는 쓰레드풀(사실 여기서 쓰이는 캐싱의 의미는 일정시간동안 쓰레드를 검사한다는 뜻.. 60초 동안 작업이 없으면 Pool에서 제거한다)
  • FixedThreadPool : 고정된 개수를 가진 쓰레드풀
  • SingleThreadExecutor : 한 개의 쓰레드로 작업을 처리하는 쓰레드풀.. 이라기보단 TaskPool의 개념이 더 적합할 것 같다.


도식은 간단하게 위와같이 나타낼 수 있을 것 같다.

여기서 Customer라는건 Application에서 해당 ExecutorService를 사용하는 클래스 정도로 해석하면 될 것 같고,

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


이 때, Task를 가진 Queue에서 ThreadPool에 있는 쓰레드들이 각기 본인의 Task를 가지고 작업을 처리하기 때문에, 

개발자 입장에서는 해당 쓰레드들의 생명주기를 따로 관리할 필요가 없는 것이다.


ExecutorService에 들어가는 type에는 2가지를 제공하는데, Runnable 과 Callable을 상속하여 구현한 클래스를 받을 수 있도록 되어 있다.


Runnable : return 값이 없는 쓰레드

Callable : return 값이 존재하는 쓰레드



리턴값이 있어야하는 경우엔 Callable을, return 값이 없고 쓰레드 독자적으로 작업을 처리할 수 있는 경우에는 Runnable을 상속하여 워커 쓰레드 클래스를 만들어 사용하자.


테스트 프로젝트 구조



간단하게 작업을 처리하는 Worker(Runnable, Callable을 상속받은 클래스), Job(작업을 의미하는 클래스), CustomerService(executorService를 가진 메인 클래스) 로 구성했다.




package executorLab;

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

/**
 * Created by Gompang on 2017. 3. 26..
 */
public class CustomerService {

    private ExecutorService executorService;

    public CustomerService() {
        this.executorService = Executors.newFixedThreadPool(4);
    }

    public void startWork(){
        // Start work with ExecutorService

        int threadIdSeq = 0;
        while(true){
            // Create executorLab.Job for executorLab.Worker
            Random random = new Random();

            Job job = new Job();
            job.setFirst(random.nextInt());
            job.setSecond(random.nextInt());

            // Set job for worker and ready
            Worker worker = new Worker(threadIdSeq, job);

            // Start job worker to ExecutorService
            executorService.submit(worker);

            threadIdSeq++;

            // Wait for some time to prevent overload
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String args[]){
        CustomerService customerService = new CustomerService();
        customerService.startWork();
    }
}


package executorLab;

/**
 * Created by Gompang on 2017. 3. 26..
 */
public class Job {

    private int first;
    private int second;

    public int getFirst() {
        return first;
    }

    public void setFirst(int first) {
        this.first = first;
    }

    public int getSecond() {
        return second;
    }

    public void setSecond(int second) {
        this.second = second;
    }
}



package executorLab;

/**
 * Created by Gompang on 2017. 3. 26..
 * ExecutorService의 TASK를 처리할 executorLab.Worker
 */
public class Worker implements Runnable{

    public Worker(int id, Job job) {
        this.id = id;
        this.job = job;
    }

    private int id;
    private Job job;

    public Worker() {

    }

    public void run() {
        // executorLab.Worker doing simple work.
        // add first and second.
        // finally print value and finish
        int sum = this.job.getFirst() + this.job.getSecond();

        System.out.println("id : " + id + " = " + sum);
    }
}





VisualVM을 이용해서 해당 executorService가 제대로 동작하는지 확인해보면, 생성한 쓰레드 개수만큼 잘 생성되어 실행된다

(위의 경우 fixed로 4개를 설정하여 생성한 service이다. pool-1 Thread-1,2,3,4 순으로 생성된 Pool에서 여러개의 작업이 병렬로 잘 처리된다)


fixedThreadPool만 테스트했지만, CachedThreadPool과 SingleThread는 따로 안해봐도 뻔할 것 같아서 하진 않았다.


주의할점은, cachedThreadPool은 쓰레드수가 폭발적으로 증가할 수 있다는 점이다.

Thread의 제한 없이 무한정 생성하고, 해당 쓰레드의 작업이 60초간 없을 경우 Pool에서 제거하는 방식이기 때문에 작업이 계속적으로 쌓이는 환경에서는

해당 Thread가 소멸되는 것보다, 생성되는 양이 더 많을 것으로 사료된다.


SingleThread는.. 음.. 사실 큰 의미는 없다고 생각하면서도, 싱글 쓰레드의 작업을 처리할때 고려해야 할 race-condition이라던지 하는 부분들을 알아서 처리해주는 만큼, 독자적으로 구현하는 것보다 실 서비스에선 해당 클래스를 사용해봄직 하다고 생각한다.


그리고 fixedThreadPool을 생성할때, 해당 머신의 CPU코어수를 기준으로 생성하면 더 좋은 퍼포먼스를 얻을 수 있다.


쓰레드갯수를 16개로 A에서 생성하고, B 라는 머신에서 생성한다고 했을 때 A는 코어수 1개, B는 코어수 4개라고 가정하면
이는 A 머신에서는 Thread를 생성하고 삭제하는 데 드는 switching 비용과.. 관리 비용이 더 들어갈 수도 있는 것이다.



Java에서 실행하는 머신의 CPU 코어수를 얻어오는 함수 -> 종종 쓰이니 알아두도록 하자


int coreCount = Runtime.getRuntime().availableProcessors();


반응형