본문 바로가기

JAVA/이것이 자바다

Chapter .12-2 멀티 스레드

12.7 데몬 스레드

 

- 주 스레드의 작업 돕는 보조적인 역할 수행하는 스레드

- 주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료

  • 워드프로세서의 자동저장, 미디어플레이어의 동영상 및 음악 재생, GC

 

데몬(daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.

주 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료되는데, 그 이유는 주 스레드의 보조 역할을 수행하므로 주 세레드가 종료되면 데몬 스레드의 존재 의미가 없어지기 때문이다.

 

이 점은 제외하면 데몬 스레드는 일반 스레드와 크게 차이가 없다.

 

스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출해주면 된다.

public static void main(String[] args){
    AutoSaveThread thread = new AutoSaveThread();
    thread.setDaemon(true);   //주 스레드인 main 스레드가 AutoSaveThread를 데몬 스레드로 만듬
    thread.start();
}

주의할 점은 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출 전에 setDaemon(true)를 호출해야 한다.

 

현재 실행중인 스레드가 데몬 스레드인지를 확인하기 위해서는 isDaemon() 메소드의 리턴값을 조사해보면 된다.

 

 

12.8 스레드 그룹

스레드 그룹(ThreadGroup)은 관련된 스레드를 묶어서 관리할 목적으로 이용된다.

 

JVM이 실행되면 system 스레드 그룹을 만들고 JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킨다.

 

그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main스레드 그룹에 포함시킨다.

 

스레드는 반드시 하나의 스레드 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자기 자신을 생성한 스레드와 같은 그룹에 속한다.

 

우리가 생성하는 작업의 대부분은 main 스레드가 생성하므로 기본적으로 main 스레드 그룹에 속하게 된다.

 

12.8.1 스레드 그룹 이름 얻기

현재 스레드가 속한 스레드 그룹의 이름을 얻고 싶다면 아래 코드를 실행하면 된다.

ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();

 

Thread의 정적 메소드인 getAllStackTraces()를 이용하면 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻을 수 있다.

Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();

getAllStackTreaces() 메소드는 Map 타입의 객체를 리턴하는데 키는 스레드 객체이고 값은 스레드 상태 기록들을 갖고 있는 StackTraceElement[] 배열이다.

 

Map 타입에 대한 자세한 내용은 15장에서 학습한다.

 

다음 에제는 현재 실행하고 있는 스레드의 이름과 데몬 여부 그리고 속한 스레드 그룹 이름이 무엇인지 출력한다.

import java.util.Map;
import java.util.Set;

public class ThreadInfoExample {
	public static void main(String[] args) {
		AutoSaveThread autoSaveThread = new AutoSaveThread();
		autoSaveThread.setName("AutoSaveThread");
		autoSaveThread.setDaemon(true);
		autoSaveThread.start();
		
		Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
		Set<Thread> threads = map.keySet();
		for(Thread thread : threads) {
			System.out.println("Name: " + thread.getName() + ((thread.isDaemon())?"(데몬)": "(주)"));
			System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
			System.out.println();
		}
	}
}

 

 

12.8.2 스레드 그룹 생성

명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 ThreadGroup 객체를 만들면 된다.

ThreadGroup 이름만 주거나, 부모 ThreadGroup과 이름을 매개값을 줄 수 있다.

ThreadGroup tg = new ThreadGroup(String Name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String Name);

 

스레드 그룹 생성시 부모(parent) 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성된다.

에를 들어 main 스레드가 ThreadGroup(String name)을 이용해서 새로운 스레드 그룹을 생성하면, main 스레드 그룹의 하위 스레드 그룹이 된다.

 

새로운 스레드 그룹을 생성한 후, 이 그룹에 스레드를 포함시키려면 Thread 객체를 생성할 때 생성자 매개값으로 스레드 그룹을 지정하면 된다.

스레드 그룹을 매개값으로 갖는 Thread 생성자는 다음 4가지가 있다.

Thread t = new Thread(ThreadGroup group, Runnable tabget);
Thread t = new Thread(ThreadGroup group, Runnable tabget, String name);
Thread t = new Thread(ThreadGroup group, Runnable tabget, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);

 

Runnable 타입의 target은 Runnable 구현 객체를 말하며, String 타입의 name은 스레드의 이름이다.

그리고 long 타입의 stackSize는 JVM이 이 스레드에 할당할 stack 크기이다.

 

12.8.3 스레드 그룹의 일괄 interrupt()

스레드를 스레드 그룹에 포함시키면 어떤 이점이 있을까?

스레드 그룹에서 제공하는 interrupt() 메소드를 이용하면 그룹 내에 포함된 모든 스레드들을 일괄 interrupt할 수 있다.

스레드 그룹에 속한 스레드를 일괄 interrupt처리 할 수 있다.

 

메소드 설명 
int activeCount() 현재 그룹 및 하위 그룹에서 활동중인 모든 스레드의 수를 리턴한다.
int activeGroupCount() 현재 그룹에서 활동중인 모든 하위 그룹의 수를 리턴한다.
void checkAccess() 현재 스레드가 스레드 그룹을 변경할 권한이 있는지 체크한다.
만약 권한이 없으면 SecurityException을 발생시킨다.
void destory() 현재 그룹 및 하위 그룹을 모두 삭제한다.
단, 그룹 내에 포함된 모든 스레드들이 종료 상태가 되어야한다.
boolean isDestoryed() 현재 그룹이 삭제되었는지 여부를 리턴한다.
int getmaxPriority() 현재 그룹에 포함된 스레드가 가질수 있는 최대 우선순위를 리턴한다.
void setmaxPriority(int pri) 현재 그룹에 포함된 스레드가 가질 수 있는 최대 우선순위를 설정한다.
String getName() 현재 그룹의 이름을 리턴한다.
ThreadGroup getParent() 현재 그룹의 부모 그룹을 리턴한다.
boolean parentOf(ThreadGroup g) 현재 그룹이 매개값으로 지정한 스레드 그룹의 부모인지 여부를 리턴한다.
boolean isDeamon() 현재 그룹이 데몬 그룹인지 여부를 리턴한다.
void setDaemon(boolean daemon) 현재 그룹을 데몬 그룹으로 설정한다.
void list() 현재 그룹에 포함된 스레드와 하위 그룹에 대한 정보를 출력한다.
void interrupt() 현재 그룹에 포함된 몯느 스레드들을 interrupt 한다.

 

 

12.9 스레드 풀

■ 스레드 폭증으로 일어나는 현상

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

  - 스레드 생성과 스케줄링으로 인해 CPU가 바빠짐

  - 메모리 사용량이 늘어남

  - 애플리케이션의 성능 급격히 저하

 

■ 스레드 풀(Thread Pool)

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

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

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

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

 

 

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

 

Executors의 다양한 정적 메소드를 이용해서 ExecutorService 구현 객체를 만들 수 있는데, 이것이 바로 스레드풀이다.

ExecutorService 동작 원리

 

12.9.1 스레드 풀 생성 및 종료

스레드풀 생성

ExecutorService 구현 객체는 Executors 클래스의 다음 두 가지 메소드 중 하나를 이용해서 간편하게 생성할 수 있다.

- newCachedThreadPool()

  • int 값이 가질 수 있는 최대 값만큼 스레드 추가, 운영체제의 상황에 따라 달라짐

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

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

  • 추가된 스레드를 종료하고 풀에서 제거

 

- newFixedThreadPool(int nThreads)

  • 코어 스레드 개수와 최대 스레드 개수가 매개값으로 준 nThread

  • 스레드가 작업 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않음

 

 

 

초기 스레드 수는 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수를 말하고,

코어 스레드 수는 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드 풀에서 제거할 때 최소한 유지해야 할 스레드 수를 말한다.

최대 스레드 수는 스레드 풀에서 관리하는 최대 스레드 수 이다.

 

newCachedThreadpool() 메소드로 생성된 스레드 풀의 특징은 최기 스레드 개수와 코어 스레드 개수는 0개이고, 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시켜 작업을 처리한다.

 

이론적으로는 int값이 가질 수 있는 최대값 만큼 스레드가 추가되지만, 운영체제의 성능과 상황에 따라 달라진다.

 

1개 이상의 스레드가 추가되었을 경우 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거한다.

 

다음은 newCachedThreadPool()을 호출해서 ExecutorService 구현 객체를 얻는 코드이다.

ExecutorService executorService = Executors.newCachedThreadPool();

newCachedThreadPool(int nThreads) 메소드로 생성된 스레드풀의 초기 스레드 ㄱ개수는 0개이고, 코어 스레드 수는 nThreads이다.

 

스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시키고 작업을 처리한다.

최대 스레드 개수는 매개값으로 준 nThreads이다.

 

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

다음은 CPU 코어의 수 만큼 최대 스레드를 사용하는 스레드풀을 생성한다.

ExecutorService executorService = Executors.newFixedThreadPool(
     Runtime.getRuntime().avaliableProcessors()
);

newCachedThreadPool()과 newFixedThreadPool()메소드를 하용하지 않고 코어 스레드개수오 ㅏ최대 스레드 개수를 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성하면 된다.

 

사실 위 두가지 메소드도 내부적으로 ThreadPoolExecutor 객체를 생성해서 리턴한다.

 

다음은 초기 스레드수 0개, 코어 스레드 수 3개, 최대 스레드 개수가 100개인 스레드 풀을 생성한다.

 

코어 스레드 3개를 제외한 나머지 추가된 스레드가 120초 동안 놀고 있을 경우 해당 스레드를 제거해서 스레드 수를 관리한다.

 

ExecutorService threadPool = new ThreadPoolExecutor(
    3,      //코어 스레드 개수
    100,    //최대 스레드 개수
    120L,   //놀고있는 시간
    TimeUnit.SECONDS, //놀고 있는 시간 단위
    new SynchronousQueue<Runnable>()   //작업 큐
);

 

스레드풀 종료

스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행상태로 남아있다.

 

그래서 main() 메소드가 실행이 끝나도 애플리케이션 프로세스는 종료되지 않는다.

 

애플리케이션을 종료하려면 스레드 풀을 종료시켜 스레드들이 종료 상태가 되도록 처리해주어야 한다.

ExecutorService는 종료와 관련해서 다음 세 개의 메소드를 제공하고 있다.

 

남아 있는 작업을 마무리하고 스레드 풀을 종료할때는 shutdown()을 일반적으로 호출하고, 남아있는 작업과는 상관없이 강제로 종료할때에는 shutdownNOw()를 호출한다.

executorSerivce.shutdown();
또는
executorSerivce.shutdownNow();

 

 

12.9.2 작업 생성과 처리 요청

작업 생성

하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현된다.

Runnable과  Callable의 차이점은 하나의 작업 처리 완료 후 리턴값이 있느냐 없느냐이다.

Runnable과 Claable의 차이

Runnable의 run() 메소드는 리턴값이 없고, Callable의 call() 메소드는 리턴값이 있다.

call()의 리턴 타입은 implements Callable<T>에서 지정한 T 타입이다.

스레드풀의 스레드는 작업 큐에서 Runnable 또는 Callable 객체를 가져와 run()과 call() 메소드를 실행한다.

 

작업 처리 요청

작업처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

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

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

  • execute()

    - 스레드 종료 후 해당 스레드 제거

    - 스레드 풀은 다른 작업 처리를 위해 새로운 스레드 생성

  • submit()

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

 

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

public class ExecuteVsSubmitExample {
	public static void main(String[] args) throws Exception {
		ExecutorService executorService = Executors.newFixedThreadPool(2);

		for(int i=0; i<10; i++) {
			Runnable runnable = new Runnable() {
				@Override
				public void run() {
					//스레드 총 개수 및 작업 스레드 이름 출력
					ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
					int poolSize = threadPoolExecutor.getPoolSize();
					String threadName = Thread.currentThread().getName();
					System.out.println("[총 스레드 개수: " + poolSize + "] 작업 스레드 이름: " + threadName);
					//예외 발생 시킴
					int value = Integer.parseInt("삼");
				}
			};
			
			executorService.execute(runnable);
			//executorService.submit(runnable);
			
			Thread.sleep(10);
		}
		
		executorService.shutdown();
	}
}

execute() 메소드를 이용한 작업 처리 요청

 

스레드풀의 스레드 최대 개수는 2는 변함이 없지만,

실행 스레드의 이름을 보면 모두 다른 스레드가 작업을 처리하고 있다.

이것은 작업 처리 도중 예외가 발생했기 때문에 해당 스레드는 제거가 되고 새 스레드가 계속 생성되기 때문이다.

 

submit() 메소드로 작업 처리를 요청한 경우를 보자

21라인을 주석 처리하고 22라인을 주석 해제한 다음 실행해보자.

 

예외가 발생하더라도 스레드가 종료되지 않고 계속 재사용되어 다른 작업을 처리하고 있는것을 볼 수 있다.

 

 

12.9.3 블로킹 방식의 작업 완료 통보

ExecutorService의 submit() 메소드는 매개값으로 준 Runnable 똔느 Callable작업을 스레드풀의 작업 큐에 저장하고 즉시 Future 객체를 리턴한다.

 

Future 객체는 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가(지연했다가=블로킹되었다가) 최종 결과를 얻는데 사용된다.

그래서 Future를 지연완료(pending completion) 객체라고 한다.

 

Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹 되었다가 작업ㅇ르 완료하면 처리 결과를 리턴한다.

 

이것이 블로킹을 사용한 작업 완료 통보 방식이다.

리턴타입인 V는 submit(Runnable task, V result)의 두 번째 타입인 V 타입이거나 submit(Callable<V> task)의 Callable 타입 파라미터 V 타입이다.

메소드 작업 처리 완료후 리턴 타입 작업 처리 도중 예외 발생
submit(Runnable task) future.get() -> null future.get() -> 예외 발생
submit(Runnable task, Integer result) future.get() -> int 타입 값 future.get() -> 예외 발생
submit(Callable<String> task) future.get() -> String 타입 값 future.get() -> 예외 발생

 

Future를 이용한 블로킹 방식의 작업 완료 통보에서 주의할 점은 작업을 처리하는 스레드가 작업을 완료하기 전까지는 get() 메소드가 블로킹 되므로 다른 코드를 실행할 수 없다.

 

만약 UI를 변경하고 이벤트를 처리하는 스레드가 get() 메소드를 호출하면 작업을 완료하기 전까지 UI를 변경할 수도 없고 이벤트를 처리할 수도 없게된다.

 

그렇기 때문에 get() 메소드를 호출하는 스레드는 새로운 스레드이거나 스레드풀의 또 다른 스레드가 되어야 한다.

 

Future 객체는 작업 결과를 얻기 위핸 get() 메소드 이외에도 다음과 같은 메소드를 제공한다.

Future 객체에 속한 다른 메소드

 

 

■작업완료 통보 방식에 따른 구분

- 리턴값이 없는 작업 완료 통보

  • Runnable 객체로 생성해 처리 (p.634~636)

 

- 리턴값이 있는 작업 완료 통보

  • 작업 객체를 Callable 로 생성 (p.636~638)

 

 

리턴값이 없는 작업 완료 통보

리턴값이 없는 작업일 경우는 Runnable 객체로 생성하면 된다.

Runnable task = new Runnable(){
    @override
    public void run(){
        //스레드가 처리할 내용
    }
};

결과 값이 없는 작업 처리는 submit(Runnable task) 메소드를 이용하면 된다.

결과값이 없음에도 불구하고 다음과 같이 Future 객체를 리턴하는데 이것은 스레드가 작업 처리르 정상적으로 완료 했는지, 아니면 작업 처리 도중에 예외가 발생했는지 확인하기 위해서이다.

Future future = executorService.submit(task);

 

작업 처리가 정상적으로 완료 되었다면 Future의 get() 메소드는 null을 리턴하지만 스레드가 작업처리도중 interrupt 되면 InterruptException을 발생시키고, 작업 처리 도중에 예외가 발생하면 ExceutionException을 발생시킨다.

그래서 예외 처리 코드가 필요하다.

try{
    future.get();
}catch(InterruptedException e){
    //작업 처리 도중 스레드가 interrupt 될 경우 실행할 코드
}catch(ExecutionException e){
    //작업 처리 도중 예외가 발생될 경우 실행할 코드
}

 

다음 예제는 리턴값이 없고 단순히 1부터 10까지의 합을 출력하는 작업을 Runnable 객체로 생성하고, 스레드 풀의 스레드가 처리하도록 요청한 것이다.

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

public class NoResultExample {
	public static void main(String[] args) {
		ExecutorService  executorService = Executors.newFixedThreadPool(
			Runtime.getRuntime().availableProcessors()
		);
		
		System.out.println("[작업 처리 요청]");
		Runnable runnable = new Runnable() {
			@Override
			public void run() {
				int sum = 0;
				for(int i=1; i<=10; i++) {
					sum += i;
				}
				System.out.println("[처리 결과] " + sum);
			}
		};
		Future future = executorService.submit(runnable);
		
		try {
			future.get();
			System.out.println("[작업 처리 완료]");
		} catch(Exception e) {
			System.out.println("[실행 예외 발생함] " + e.getMessage());
		}
		
		executorService.shutdown();
	}
}

 

 

리턴값이 있는 작업 완료 통보

스레드풀의 스레드가 작업을 오나료한 후에 애플리케이션이 처리 결과를 얻어야 된다면 작업 객체를 Callable로 생성하면 된다.

제네릭 타입 파라미터 T 는 call() 메소드가 리턴하는 타입이 되어야 한다.

Callable<T> task = new Callable<T>{
    @Override
    public T call() throws Exceptoin{
        //스레드가 처리 할 내용
        return T;
    }
};

Callable 작업의 처리 요청은 Runnable 작업과 마찬가지로 ExecutorService의 submit() 메소드를 호출하면 된다.

submit() 메소드는 작업 큐에 Callable객체를 저장하고 즉시 Future<T>를 리턴한다.

 

이때 T는 call() 메소드가 리턴하는 타입이다.

Future<T> future = executorService.submit(task);

 

스레드풀의 스레드가 Callable 객체의 call() 메소드를 모두 실행하고 T 타입의 값을 리턴하면, Future<T>의 get() 메소드는 블로킹이 해제되고 T 타입의 값을 리턴하게 된다.

try{
    T result = future.get();
}catch(InterruptedException e){
    //작업 처리 도중 스레드가 interrupt 될 경우 실행될 코드
}catch(ExecutionException e){
    // 작업 처리 도중 예외가 발생된 경우 실행할 코드
}

다음 예제는 1부터 10까지의 합을 리턴하는 작업을 Callable 객체로 생성하고, 스레드풀의 스레드가 처리하도록 요청한 것이다.

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ResultByCallableExample {
	public static void main(String[] args) {
		ExecutorService  executorService = Executors.newFixedThreadPool(
			Runtime.getRuntime().availableProcessors()
		);
		
		System.out.println("[작업 처리 요청]");
		Callable<Integer> task = new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				int sum = 0;
				for(int i=1; i<=10; i++) {
					sum += i;
				}
				return sum;
			}
		};
		Future<Integer> future = executorService.submit(task);
		
		try {
			int sum = future.get();
			System.out.println("[처리 결과] " + sum);
			System.out.println("[작업 처리 완료]");
		} catch (Exception e) {
			System.out.println("[실행 예외 발생함] " + e.getMessage());
		}
		
		executorService.shutdown();
	}
}

 

작업 처리 결과를 외부 객체에 저장

상황에 따라서 스레드가 작업한 결과를 외부 객체에 저장해야 할 경우도 있다.

예를 들어 스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 저장하면, 애플리케이션이 Result 객체를 사용해서 어떤 작업을 진행 할 수 있을 것이다.

 

대개 Result 객체는 공유 객체가 되어, 두 개 이상의 스레드 작업을 취합할 목적으로 이용된다.

 

이런 작업ㅇ르 하기 위해서 ExecutorService의 submit(Runnable task, V result) 메소드를 사용할 수 있는데, V가 바로 Result 타입이 된다.

 

메소드를 호출하면 즉시 Future<V>가 리턴 되는데 Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 V타입 객체를 리턴한다.

 

리턴된 객체는 submit()의 두 번쨰 매개값으로 준 객체오 ㅏ동일한데, 차이점은 스레드 처리 결과가 내부에 저장되어 있다는 것이다.

Result result = ...;
Runnable task = new Task(result);
Future<Result> future = executorService.submit(task,result);
result = future.get();

작업 객체는 Runnable 구현 클래스로 생성하는데, 주의할 점은 스레드에서 결과를 저장하기 위해 외부 Result 객체를 사용해야 하므로 생성자를 통해 Result 객체를 주입받도록 해야 한다.

class Task implement Runnable{
    Result result;
    Task(Result result){this.result = result; }
        
    @Override
    public void run(){
        //작업 코드
        //처리 결과를 result에 저장
    }
}

 

다음 예제는 1부터 10까지의 합을 계산한느 두 개의 작업을 스레드 풀에 처리 요청하고, 각각의 스레드가 작업 처리를 완료한 후 산출된 값을 외부 Result 객체에 누적하도록 했다.

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

public class ResultByRunnableExample {
	public static void main(String[] args) {
		ExecutorService  executorService = Executors.newFixedThreadPool(
			Runtime.getRuntime().availableProcessors()
		);
		
		System.out.println("[작업 처리 요청]");
		class Task implements Runnable {
			Result result;
			Task(Result result) { 
				this.result = result;
			}
			@Override
			public void run() {
				int sum = 0;
				for(int i=1; i<=10; i++) {
					sum += i;
				}
				result.addValue(sum);
			}
		}
		
		Result result = new Result();
		Runnable task1 = new Task(result);
		Runnable task2 = new Task(result);
		Future<Result> future1 = executorService.submit(task1, result);
		Future<Result> future2 = executorService.submit(task2, result);
		
		try {
			result = future1.get();
			result = future2.get();
			System.out.println("[처리 결과] " + result.accumValue);
			System.out.println("[작업 처리 완료]");
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("[실행 예외 발생함] " + e.getMessage());
		}
		
		executorService.shutdown();
	}
}

class Result { 
	int accumValue;
	synchronized void addValue(int value) {
		accumValue += value;
	}
}

 

 

작업 완료 순으로 통보

작업 요청 순서대로 작업 처리가 완료되는 것은 아님

작업의 양과 스레드 스케줄링에 따라 먼저 요청한 작업이 나중에 완료되는 경우도 발생

여러 개의 작업들이 순차적으로 처리될 필요성이 없고,

     처리 결과도 순차적으로 이용할 필요가 없다면

     작업 처리가 완료된 것부터 결과를 얻어 이용하는 것이 좋음

 

 

작업 요청 순선대로 작업 처리가 완료되는 것은 아니다.

작업의 양과 스레드 스케줄링에 따라서 먼저 요청한 작업이 나중에 완료되는 경우도 발생한다.

여러 개의 작업들이 순차적으로 처리도리 필요성이 없고, 처리 결과도 순차적으로 이용할 필요가 없다면 작업 처리가 완료된 것부터 결괄르 얻어 이용하면 된다.

 

스레드 풀에서 작업 처리가 완료된 것만 통보받는 방법이 있는데 CompletionService를 이용하는 것이다.

CompletionService는 처리 완료된 작업을 가져오는 poll()과 take() 메소드를 제공한다.

poll()과 take() 메소드

 

CompletionService 구현 클래스는 ExecutorCompletionService<V> 이다.

객체를 생성할 때 생성자 매개값으로 ExecutorService를 제공하면 된다.

ExecutorService executorService = Executors.newFixedThreadPool(
    Runtime.getRuntime().avaliableProcessors()
);

CompletionService<V> completionService = new ExecutorCompletionService<V>(
    executorService
);

 

poll()과 take() 메소드를 이용해서 처리 완료된 작업의 Future를 얻으려면 CompletionService의 submit() 메소드로 작업처리 요청을 해야 한다.

completionService.submit(Callable<V> taksk);
completionService.submit(Runnable taksk, V result);

 

 

12.9.4 콜백 방식의 작업 완료 통보

이번에는 콜백(callback) 방식을 이용해서 작업 완료 통보를 받는 방법에 대해서 알아보자.

콜백이란 애플리케이션이 스레드에게 작업 처리를 요청한 후 스레드가 작업 처리를 완료하면 특정 메소드를 자동 실행하는 기법을 말한다.

 

이때 자동 실행되는 메소드를 콜백 메소드라고 한다.

블로킹 방식과 콜백 방식 비교

 

블로킹 방식은 작업 처리를 요청한 후 작업이 완료될 때까지 블로킹 되지만, 콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능을 수행할 수 있다.

 

그 이유는 작업 처리가 완료되면 자동적으로 콜백 메소드가 실행되어 결과를 알 수 있기 때문이다.

 

아쉽게도 ExecutorService는 콜백을 위한 별도의 기능를 제공하지 않는다.

하지만 Runnable구현 클래스를 작성할 때 콜백 기능을 구현할 수 있다.

 

먼저 콜백 메소드를 가진 클래스가 있어야 하는데, 직접 정의해도 좋고, java.nio.channels.CompletionsHander를 이용해도 좋다.

 

이 인터페이스는 NIO 패키지에 포함되어 있는데 비동기 통신에서 콜백 객체를 만들 떄 사용된다.

 

그럼 CompletionHandler를 이용해서 콜백 객체를 만드는 방법을 살펴보자.

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는 첨부값의 타입이다.

첨부값은 콜백 메소드에 결과값 이외에 추가적으로 전달하는 객체라고 생각하면 된다.

만약 첨부값이 필요 없다면 A는 Void로 지정해주면 된다.

 

다음은 작업 처리 결과에 따라 콜백 메소드를 호출하는 Runnable 객체이다.

Runnable task = new Runnable(){
    @Override
    public void run(){
        try{//작업 처리
            V result = .....;
        }catch(Exception e){ //예외가 발생 했을 경우 호출
            callback.failed(e,null);
        }
    }
};

 

작업 처리가 정상적으로 완료되면 completed() 콜백 메소드를 호출해서 결과값을 전달하고,

예외가 발생하면 failed() 콜백 메소드를 호출해서 예외 객체를 전달한다.