멀티 스레드 개념
프로세스와 스레드
프로세스( precess )란?
- 운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스라고 부른다.
- 즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다.
- 하나의 애플리케이션은 다중 프로세스를 만들기도 한다.
- 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원, 그리고 스레드로 구성된다.
스레드란?
- 프로세스 내에서 실제로 작업을 수행하는 주체를 뜻한다.
- 모든 프로세스는 한 개 이상의 스레드가 존재하여 작업을 수행한다.
- 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 부른다.
멀티 태스킹( multi tasking )이란?
- 두 가지 이상의 작업을 동시에 처리하는 것을 의미한다.
- 운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다.
- 멀티 태스킹은 꼭 멀티 프로세스를 의미하지는 않는다.
멀티 프로세스 ( multi process ) vs 멀티 스레드( multi thread )
여러 흐름을 동시에 수행한다는 공통점을 가지고 있다.
- 멀티 프로세스: 여러 개의 CPU를 사용하여 여러 프로세스를 동시에 수행하는 것
- 각 프로세스가 독립적인 메모리를 가지고 별도로 실행된다.
- 하나의 프로세스에서 오류가 발생해도 다른 프로세스에 영향을 미치지 않는다.
- (1개의 프로세스가 죽어도 다른 프로세스들은 계속 실행된다.)
- Context Switching을 위한 오버헤드가 발생한다.
- 멀티 스레드: 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것
- 각 스레드가 자신이 속한 프로세스의 메모리를 공유한다.
- 시스템 자원 낭비가 적다.
- 하나의 스레드에 문제가 생기면, 전체 프로세스가 영향을 받는다.
프로세스와 스레드의 차이
프로세스 : 별도의 메모리 공간에서 실행
스레드 : 동일한 프로세스 내의 공유 메모리 공간에서 실행
프로세스는 운영체제로부터 자원을 할당받는 작업의 단위이다.
쓰레드는 프로세스가 할당받은 자원을 이용하는 실행의 단위이고 프로세스 내에 여러개 생길 수 있다..
싱글스레드(Single thread)
하나의 프로세스에서 오직 하나의 스레드로만 실행한다.
그렇기 때문에, 하나의 레지스터와 스택으로 표현이 가능하다.
멀티 스레드 (Multi thread)
멀티 스레드는 CPU의 최대 활용을 위해 프로그램의 둘 이상을 동시에 실행하는 기술이다
메인 스레드
모든 자바 애플리케이션은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고,
main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행이 종료된다.
메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다.
즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.
싱글 스레드 애플리케이션:
메인 스레드가 종료하면 프로세스도 종료된다.
멀티 스레드 애플리케이션:
실행 중인 스레드가 하나라도 있다면, 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다
먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.

작업스레드 생성과 실행
멀티 스레드로 실행하는 애플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.
어떤 자바 애플리케이션이건 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다.

Thread 클래스로부터 직접 생성
java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.
Runnable 인터페이스란?
Runnable 인터페이스는 몸체가 없는 메소드인 run() 메소드 단 하나만을 갖는 간단한 인터페이스로, 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다.
Thread thread = new Thread(Runnable target);
Thread 생성
Runnable은 작업 내용을 가지고 있는 객체이지, 실제 스레드는 아니다.
기본 형태
Runnable 구현 객체를 생성한 후, 이를 매개값으로 Thread 생성자를 호출해야 작업 스레드가 생성된다.
Runnable task = new Task();
Thread thread = new Thread(task);
익명 객체 사용
코드 절약을 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다. (많이 사용되는 방식)
Thread thread = new Thread(new Runnable() {
public void run() {
//스레드가 실행할 코드;
}
});
람다식 사용
Runnable 인터페이스는 함수적 인터페이스이기 때문에 람다식을 매개값으로 사용할 수도 있다.
- 함수적 인터페이스: 추상메소드가 1개만 정의된 인터페이스
- 자바 8부터 지원되기 때문에 자바 7 이전 버전에서는 사용할 수 없다.
Thread thread = new Thread(() -> {
//스레드가 실행할 코드;
});
Thread 실행
작업 스레드는 start() 메소드를 호출해야만 비로소 실행된다. (생성되는 즉시 실행되는 것이 아니다!)
thread.start();
start() 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리한다.
Thread 하위 클래스로부터 생성
작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있다.
작업 Thread 클래스 정의 방법
1. 기본 형태
Thread 클래스를 상속한 후 run 메소드를 재정의(overriding)해서 스레드가 실행할 코드를 작성한다.
작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성하는 방법과 동일
public class WorkerThread extends Thread {
@Override
public void run() {
//스레드가 실행할 코드 // <---- run()메소드 재정의
}
}
Thread thread = new WorkerThread();
2. 익명 객체 이용
코드 절약을 위해 Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있다.
Thread thread = new Thread() {
public void run() {
//스레드가 실행할 코드; // <--- 익명 자식 객체
}
}
Thread 실행
이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면 작업 스레드는 자신의 run() 메소드를 실행하게 된다.
thread.start();

스레드의 이름
스레드는 자신의 이름을 가지고 있다. 스레드의 이름이 큰 역할을 하는 것은 아니지만,
디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용된다.
메인 스레드는 main이라는 이름을 가지고 있고,
우리가 직접 생성한 스레드는 자동적으로 Thread-n이라는 이름으로 설정된다. (n: 스레드의 번호)
Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경하면 된다.
스레드 이름 설정하기
thread.setName("스레드 이름");
스레드 이름 알아내기
thread.getName();
setName()과 getName()은 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread의 정적 메소드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.
Thread thread = Thread.currentThread();
스레드 우선순위
멀티 스레드는 동시성 또는 병렬성으로 실행되기 때문에 이 용어들에 대해 정확히 이해하는 것이 좋다.
동시성 ( Concurrency ) -동시에 실행되는 것처럼 보이는 것
멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말한다.
싱글 코어 CPU를 이용한 멀티 스레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하는 것이 워낙 빠르다보니 병렬성으로 보일 뿐이다.
병렬성 ( Parallelism ) -실제로 동시에 여러 작업이 처리되는 것
병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말한다.

스레드의 개수가 코어의 수보다 많을 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄링이라고 한다. 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.

스레드 스케줄링
자바의 스레드 스케줄링은 우선순위 방식과 순환 할당 방식을 사용한다.
우선순위 방식 ( Priority )
우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것
만약 우선순위를 변경하고 싶다면 Thread 클래스가 제공하는 setPriority() 메소드를 이용하면 된다.
thread.setPrioity(우선순위);
thread.setPriority(Thread.MAX_PRIORITY); //10
thread.setPriority(Thread.NORM_PRIORITY); //5
thread.setPriority(Thread.MIN_PRIORITY); //1
동기화 메소드와 동기화 블록
공유 객체를 사용할 때의 주의할 점
싱글 스레드 프로그램에서는 한 개의 스레드가 객체를 독차지해서 사용하면 되지만,
멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다.

동기화 메소드 및 동기화 블록
멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다. 자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.
스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역 코드를 실행하지 못하도록 한다. 동기화 메소드를 만드는 방법은 다음과 같이 메소드 선언에 synchronized 키워드를 붙이면 된다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
public synchronized void method() {
//임계 영역;
//단 하나의 스레드만 실행
}
동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다. 메소드 전체 내용이 아니라, 일부 내용만 임계 영역으로 만들고 싶다면 다음과 같이 동기화(synchronized) 블록을 만들면 된다.
public void method() {
//여러 스레드가 실행 가능 영역
...
//동기화 블록
synchronized(공유객체) { //공유 객체가 객체 자신이면 this를 넣을 수 있다.
//임계 영역
//단 하나의 스레드만 실행
}
}

스레드 상태

스레드 객체를 생성하고, start() 메소드를 호출하면 스레드가 실행되는 것처럼 보이지만 대기 상태가 됩니다. 실행 대기 상태란 아직 스케줄링 되지 않아서 실행을 기다리고 있는 상태를 말합니다.
실행 대기 상태에 있는 스레드 중에서 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행하는데, 이를 실행(Running) 상태라고 합니다. 실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있습니다.
실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 됩니다. 이 상태를 종료 상태라고 합니다.

스레드 상태 제어


주어진 시간동안 일시 정지(sleep())
실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread() 클래스의 정적 메소드인 sleep()을 사용하면 됩니다. 파라미터로 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주시면 됩니다.
다른 스레드에게 실행 양보(yield())
yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질수 있도록 해줍니다.
다른 스레드의 종료를 기다림(join())
스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있습니다.
이런 경우에 join() 메소드를 제공합니다. join() 메소드를 호출하면 호출된 스레드가 종료될 때까지 일시 정지 상태로 대기하다가 실행이 됩니다.
스레드 간 협업(wait(), notify(), notifyAll)
경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있습니다.
이때, 스레드 간 협업이 잘 이루어지기 위해서는 wait(), notify(), notifyAll() 세 가지 메소드를 적절히 사용해야 합니다.


스레드의 안전한 종료(stop 플래그, interrupt())
스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료됩니다.
경우에 따라 실행 중인 스레드를 즉시 종료할 필요가 생길 수 있습니다.
예를 들면 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구할 경우가 있을 수 있습니다.
Thread는 스레드를 즉시 종료시키기 위해 stop()메서드를 제공하지만
이 메서드는 Deprecated되어있습니다.
=>아래 두가지 방법은 스레드를 즉시 종료시키기 위한 최선의 방법입니다.
stop 플래그를 이용하는 방법
스레드는 run() 메서드가 끝나면 자동적으로 종료되기 때문에 run() 메서드가
정상적으로 종료되도록 유도하는 것이 최선입니다.
아래 코드는 stop 플래그를 이용해 run() 메서드의 종료를 유도합니다.
public class XXXThread extends Thread {
private boolean stop; // stop 플래그 필드
public void run() {
while(!stop){ stop가 true가 되면 run() 종료
스레드가 반복 실행하는 코드;
}
//스레드가 사용한 자원 정리
}
}
interrupt() 메소드를 이용하는 방법
스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 합니다.
이것을 이용해 run() 메서드를 정상 종료 시킬 수 있습니다.
데몬 스레드
일반쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드이다.
일반쓰레드가 모두 종료되면 데몬쓰레드는 강제로 종료된다.
그 이유는 데몬쓰레드는 일반 쓰레드의 보조역할을 수행하는데 일반 쓰레드가 모두 종료되었다면 데몬쓰레드의 존재는 의미가 없어지기 때문이다. 데몬쓰레드의 예로는 가비지 컬렉터, 워드프로세스 자동저장, 화면 자동갱신 등이 있다.
스레드 그룹
스레드 그룹은 관련된 스레드를 묶어서 관리할 목적으로 이용됩니다. JVM이 실행되면 system 스레드 그룹을 만들고, JVM운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킵니다. 그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main 스레드 그룹에 포함시킵니다. 스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드와 같은 스레드 그룹에 속하게 됩니다. 우리가 생성하는 작업 스레드는 대부분 main 스레드가 생성하므로 기본적으로 main 스레드 그룹에 속하게 됩니다.
스레드 그룹 이름 얻기
ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();
Thread의 정적 메서드인 getAllStackTraces()를 이용하면 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻을 수 있습니다.
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
getAllStackTraces() 메서드는 Map 타입의 객체를 리턴하는데, 키는 스레드 객체이고 값은 스레드의 상태 기록들을 갖고 있는 StackTraceElement [] 배열입니다.
스레드 그룹 생성
명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 ThreadGroup 객체를 만들면 됩니다. ThreadGroup 이름만 주거나, 부모 ThreadGroup과 이름을 매개 값으로 줄 수 있습니다.
ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);
Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);
스레드 그룹의 일괄intreeupt()

스레드풀
병렬 작업 처리가 많아지면 스레드 개수가 증가되고 그에 따른 스레드 생성과 스케줄링으로 인해 CPU가 바빠져 메모리 사용량이 늘어납니다. 따라서 애플리케이션의 성능이 저하됩니다. 갑작스러운 병렬 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드 풀(Thread Pool)을 사용해야 합니다.
스레드풀 생성 및 종료
스레드풀 생성
ExecutorService 구현 객체는 Executors 클래스의 다음 두 가지 메서드 중 하나를 이용해서 간편하게 생성할 수 있습니다.

ExecutorService executorService = Executors.newCachedThreadPool();
ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
ExecutorService threadPool = new ThreadPoolExecutor(
3, //코어 스레드 개수
100, //최대 스레드 개수
120L, //놀고 있는 시간
TimeUnit.SECONDS, //놀고 있는 시간 단위
new SynchronousQueue<Runnable>() //작업 큐
);
스레드풀 종료
스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있습니다. 그래서 main() 메서드가 실행이 끝나도 애플리케이션 프로세스는 종료되지 않습니다. 애플리케이션을 종료하려면 스레드 풀을 종료시켜 스레드들이 종료 상태가 되도록 처리해주어야 합니다. ExecutorService는 종료와 관련해서 다음 세 개의 메서드를 제공하고 있습니다.

executorService.shutdown(); //남은 작업을 모두 처리하고 종료
executorService.shutdownNow(); //남은 작업 상관없이 강제 종료
작업 생성과 처리 요청
작업생성
하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현합니다. Runnable과 Callable의 차이점은 작업 처리 완료 후 리턴 값이 있느냐 없느냐입니다. 다음은 작업을 정의하기 위해 Runnable 구현 클래스를 작성하는 방법입니다.
Runnable task = new Runnable() {
@Override
public void run() {
//스레드가 처리할 작업 내용
}
}
다음은 작업을 정의하기 위해 Callable 구현 클래스를 작성하는 방법입니다.
Callable<T> task = new Callable<T>() {
@Override
public T call() throws Exception {
//스레드가 처리할 작업 내용
return T;
}
}
작업 처리 요청
작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말합니다. ExecutorService는 작업 처리 요청을 위해 다음 두 가지 종류의 메서드를 제공합니다.

블로킹 방식의 작업 완료 통보
ExecutorService의 submit() 메서드는 매개 값으로 준 Runnable 또는 Callable 작업을 스레드 풀의 작업 큐에 저장하고 즉시 Future 객체를 리턴합니다.


new Thread(new Runnable() {
@Override
public void run() {
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
future.get();
} catch (Exception e) {
e.printStackTrace();
}
}
});

리턴값이 없는 작업 완료 통보
리턴 값이 없는 작업일 경우는 Runnable 객체로 생성하면 됩니다. 결과 값이 없는 작업 처리 요청은 submit(Runnable task) 메서드를 이용하면 됩니다. 결과 값이 없음에도 불구하고 다음과 같이 Future 객체를 리턴하는데, 이것은 스레드가 작업 처리를 정상적으로 완료했는지, 아니면 작업 처리 도중에 예외가 발생했는지 확인하기 위함입니다.
Future future = executorService.submit(task);
try {
future.get();
} catch (InterruptedException e) {
//작업 처리 도중 스레드가 interrupt 될 경우 실행할 코드
} catch (ExecutionException e) {
//작업 처리 도중 예외가 발생할 경우 실행할 코드
}
리턴값이 있는 작업 완료 통보
스레드 풀의 스레드가 작업을 완료한 후에 애플리케이션이 처리 결과를 얻어야 된다면 작업 객체를 Callable로 생성하면 됩니다. 다음은 Callable 객체를 생성하는 코드인데, 주의할 점은 제네릭 타입 파라미터 T는 call() 메서드가 리턴하는 타입이 되도록 해야 합니다.
Callable<T> task = new Callable<T>() {
@Override
public T call() throws Exception {
//스레드가 처리할 작업 내용
return T;
}
};
Future<T> future = executorService.submit(task);
try {
T result = future.get();
} catch(InterruptedException e) {
//작업 처리 도중 스레드가 interrupt 될 경우 실행할 코드
} catch(ExecutionException e) {
//작업 처리 도중 예외가 발생된 경우 실행할 코드
}
작업 처리 결과를 외부 객체에 저장
상황에 따라서 스레드가 작업한 결과를 외부 객체에 저장해야 할 경우도 있습니다. 예를 들어 스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 저장하면, 애플리케이션이 Result 객체를 사용해서 어떤 작업을 진행할 수 있을 것입니다. 대게 Result 객체는 공유 객체가 되어, 두 개 이상의 스레드 작업을 취합할 목적으로 이용됩니다.
이런 작업을 하기 위해서 ExecutorService의 submit(Runnable task, V result) 메서드를 사용할 수 있는데, V가 바로 Result 타입이 됩니다. 메서드를 호출하면 즉시 Future 가 리턴되는데 Future의 get() 메서드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 V 타입 객체를 리턴합니다. 리턴된 객체는 submit()의 두 번째 매개 값으로 준 객체와 동일한데, 차이점은 스레드 처리 결과가 내부에 저장되어 있다는 것입니다.
Result result = new Result();
Runnable task = new Task(result);
Future<Result> future = executorService.submit(task, result);
result = future.get();
class Task implements Runnable {
Result result;
Task(Result result) {
this.result = result;
}
@Override
public void run() {
//작업 코드
//처리 결과를 result로 저장
}
}
작업 완료 순으로 통보
작업 요청 순서대로 작업 처리가 완료되는 것은 아닙니다. 작업의 양과 스레드 스케줄링에 따라서 먼저 요청한 작업이 나중에 완료되는 경우도 발생합니다. 여러 개의 작업들이 순차적으로 처리도리 필요성이 없고, 처리 결과도 순차적으로 이용할 필요가 없다면 작업 처리가 완료된 것부터 결과를 얻어 이용하면 됩니다. 스레드 풀에서 작업 처리가 완료된 것만 통보받는 방법이 있는데, CompletionService를 이용하는 것입니다. CompletionService는 처리 완료된 작업을 가져오는 poll()과 take() 메서드를 제공합니다.

콜백 방식의 작업 완료 통보
콜백이란 애플리케이션이 스레드에게 작업 처리를 요청한 후, 스레드가 작업을 완료하면 특정 메서드를 자동 실행하는 기법을 말합니다. 이때 자동 실행되는 메서드를 콜백 메서드라고 합니다.
블로킹 방식의 진행 순서는 이렇습니다.
- 메인 스레드에서 스레드 풀에 작업을 요청한다.
- 작업 요청한 스레드 풀에서 Future를 리턴한다.
- 메인 스레드에서 Future.get() 메서드를 호출한다.
- 스레드 풀의 작업이 완료될 때까지 메인 스레드는 블로킹된다.
- 스레드 풀의 작업이 완료되면 메인 스레드의 블로킹이 해제된다.
하지만 콜백 방식의 진행 순서는 이렇습니다.
- 메인 스레드에서 스레드 풀에 작업을 요청한다.
- 작업 요청한 스레드 풀에서 Future를 리턴한다.
- 메인 스레드와 스레드 풀 각자 독립적으로 자기 작업을 처리한다.
- 스레드 풀의 작업이 완료되면 메인 스레드에서 콜백 메서드가 실행된다.
블로킹 방식은 작업 처리를 요청한 후 작업이 완료될 때까지 블로킹되지만, 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능을 수행할 수 있습니다. 그 이유는 작업 처리가 완료되면 자동적으로 콜백 메서드가 실행되어 결과를 알 수 있기 때문입니다.
하지만 아쉽게도 ExecutorService는 콜백을 위한 별도의 기능을 제공하지 않습니다. 하지만 Runnable 구현 클래스를 작성할 때 콜백 기능을 구현할 수 있습니다. 먼저 콜백 메서드를 가진 클래스가 있어야 하는데, 직접 정의해도 좋고 java.nio.channels.CompletionHandler를 이용해도 좋습니다. 이 인터페이스는 NIO 패키지에 포함되어 있는데 비동기 통신에서 콜백 객체를 만들 때 사용됩니다.
CompletionHandler<V, A> callback = new CompletionHandler<V, A>() {
@Override
public void completed(V result, A attachment) {}
@Override
public void failed(Throwable exc, A attachment) {}
};
CompletionHandler는 completed() 메서드와 failed() 메서드가 있습니다. completed() 메서드는 작업을 정상 처리 완료했을 때 호출되는 콜백 메서드이고, failed() 메서드는 작업 처리 도중 예외가 발생했을 때 호출되는 콜백 메서드입니다. CompletionHandler의 V 타입 파라미터는 결괏값의 타입이고, A는 첨붓값의 타입입니다. 첨붓값은 콜백 메서드에 결괏값 이외에 추가적으로 전달하는 객체라고 생각하면 됩니다.
Runnable task = new Runnable() {
@Override
public void run() {
try {
//작업 처리
V result = ...;
callback.completed(result, null);
} catch (Exception e) {
callback.failed(e, null);
}
}
};