Multi Thread
멀티스레드 개념
프로세스와 스레드
- 실행중인 하나의 프로그램
- 멀티 프로세스는 독립적으로 프로그램을 실행하고 여러가지 작업 처리
- 멀티스레드는 하나의 프로그램을 이용해 내부적으로 여러가지 작업처리
메인스레드
- 모든 자바 애클리케이션은 메인스레드 가 main() 메소드 실행하면서 시작됨.
-
메인 스레드는 필요에 따라 작업 스레드들을 만들어 병렬로 코드를 실행할 수 있음. (멀티 스레드를 생성해서 멀티 태스킹 수행)
-
프로세스 종료
- 싱글 스레드의 경우 메인스레드가 종료되면 프로세스도 종료
- 멀티 스레드의 경우 실행중인 스레드가 하나라도 있으면 프로세스는 종료되지 않음.
-
작업 스레드 생성과 실행
- 몇개의 작업을 병렬로 실행할지 결정해야 함.
- java.lang.Thread 클래스를 직접 객체화해서 생성
- Thread 상속해서 하위 클래스를 만들어 생성할수도 있음.
Thread 클래스로부터 직접 생성
- java.lang.Thread 클래스로부터 작업 스레드 객체를 생성하려면 다음과 같이 Runnable 을 매개값으로 갖는 생성자를 호출
- Runnable 에는 run() 메소드가 하나 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업스레드가 실행 할 코드를 작성해야함.
Thread thread = new Thread(Runnable target);
- Runnable 은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입
class Task implements Runnable {
public void run(){
스레드가 실행 할 코드;
}
}
- Runnable 은 작업 내용을 가지고있는 객체이지 실제 스레드가 아님.
- 구현 객체를 생성 후 이것을 매개값으로 해서 Thread 생성자를 호출하면 비로소 작업스레드가 생성됨.
Runnable task = new Task();
Thread thread = new Thread(task);
//익명 구현객체를 생성하는 방법으로 사용
Thread thread = new Thread(new Runnable(){
public void run(){
실행할 코드;
}
});
//람다식을 매개값으로 사용
Thread thread = new Thread( ()-> {
스레드 실행 코드;
});
-
Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스임.
-
작업스레드는 생성되는 즉시 실행오는것이 아니라 start() 메소드를 다음과 같이 호출해야 비로소 실행됨.
thread.start();
Thread 하위 클래스로부터 생성
- 작업 스레드가 실행할 작업을 Runnable로 만들지 않고 하위 클래스로 작업 스레드를 정의하면서 작업내용을 포함시킬수 있음.
- Thread 클래스 상속 후 run 메소드를 overriding 후 스레드가 실행 할 코드 작성
public class WorkerThread extends Thread{
@Override
public void run(){
//스레드 실행코드;
}
}
Thread thread = new WorkerThread();
//Thread 익명 객체로 작업 스레드를 객체 생성
Thread thread = new Thread(){
public void run(){
스레드 실행코드
}
};
//호출
thread.start();
스레드의 이름
- 메인 스레드 이름 : main
- 작업 스레드 이름 : Thread - n
thread.setName("스레드 이름"); // 자동으로설정되어있는 이름 변경 thread.getName(); // 이름 받아오기
- 코드를 실행하는 스레드의 참조 얻기
Thread thread = Thread.currentThread();
스레드 우선순위
- 멀티 스레드는 동시성(Concurrency) 또는 병렬성(Parallelism) 으로 실행됨.
- 동시성
- 멀티 작업을 위해 하나 코어에서 멀티 스레드가 번갈아가며 실행하는 성질
- 병렬성
- 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질
- 동시성
스레드 스케줄링
- 스레드가 개수의 코어 수보다 많을경우
- 스레드를 어떤 순서에 의해 동시성으로 실행할것인가를 결정해야하는데 이것을 스케줄링 이라고함.
- 자바의 스레드 스케줄링
- 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 이용
- 우선순위 방식 : 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링 하는것(코드로 제어 가능)
- 순환 할당 방식 : 시간 할당량(Time slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행(코드로 제어 불가능, JVM 에서 의해 정해짐)
- 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 이용
- 우선순위 방식은 1이 순위가 가장낮고, 10이 가장높음
- 모든 스레드들은 기본적으로 5 의 우선순위를 할당받음
//우선순위 변경방법
thread.setPriority(우선순위);
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY); //우선순위에 가독성을 위해 상수를 대입해도 상관없음
우선순위 효과
- 싱글 코어
- 우선순위가 높은 스레드가 실행기회를 더많이 가짐, 우선순위가 낮은 스레드보다 계산작업을 빨리끝냄
- 멀티 코어
- 쿼드의 경우 4개의 스레드가 병렬성으로 실행될 수 있기 때문에
- 4개 이하의 스레드를 실행할 경우엔 우선 순위 방식은 크게 영향을 미치지 못함.
- 최소 5개 이상의 스레드가 실행되어야 우선순위의 영향을 받음.
동기화 메소드와 동기화 블록
공유 객체를 사용할때 주의할점
- 멀티 스레드가 하나의 객체를 공유함에 따라 생기는 오류
- 스레드 A를 사용하던 객체가 스레드 B에 의해 상태가 변경될 수 있기 때문에 스레드 A가 의도했던 것과는 다른 결과를 산출할 수 있음
동기화 메소드 및 동기화 블록
- 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드영역을 임계 영역(critical section) 이라 함.
- 자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공함.
public synchronized void method(){
임계영역; //단 하나의 스레드만 실행
}
- 동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어남.
public void method(){
//여러 스레드가 실행 가능 영역
...
synchronized(공유 객체 ){ //공유객체가 객체 자신이면 this 를 넣을수있음.
임게 영역 // 단 하나의 스레드만 실행 동기화 블록
}
}
- 메소드 전체 내용이 아닌 일부 내용만 임계 영역으로 만들고싶다면 동기화 블록 사용
- 만약 동기화 메소드와 동기화 블록이 여러개 있을 경우 스레드가 이들중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동히과 메소드 및 블록도 실행할 수없다. 하지만 일반 메소드는 실행 가능하다.
스레드 상태
- 스레드가 객체를 생성하고 start() 메소드를 호출하면 곧바로 스레드가 실행되는것처럼 보이지만, 사실은 실행대기 상태임.
- 실행대기 상태란 스케줄링이 아직 되지않아 실행을 기다리고 있는 상태
- 스레드 중에서 스레드 스케줄링으로 선택된 스레드가 비로서 CPU를 점유하고 run()메소드를 실행함. 이때를 실행(Running) 상태라고함.
-
실행 상태에서 run() 메소드가 종료되면 더이상 실행할 코드가 없어 스레드의 실행이 멈추게되는데 이상태를 종료 상태라고 함.
- 경우에 따라 스레드는 실행상태에서 실행대기 상태로 가지않을수도 있으며 다시 실행 상태로 가기위해선 실행대기 상태로 가야함.
- 일시정지 상태로 돌아가기도하는데 스레드가 실행할수 없는 상태임. (WAITING, TIMED_WATING, BLOCKED)
상태 | 열거상수 | 설명 |
---|---|---|
객체 생성 | NEW | 스레드 객체가 생성, 아직 start()메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 | WAITING | 다른 스레드가 통지할 때까지 기다리는 상태 |
일시 정지 | TIMED_WAITING | 주어진 시간 동안 기다리는 상태 |
일시 정지 | BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 |
종료 | TERMINATED | 실행을 마친 상태 |
//스레드 상태 얻기
Thread.State state = 스레드명.getState();
스레드 상태 제어
- 실행중인 스레드 상태를 변경하는것을 스레드 상태 제어라고 함.
메소드 | 설명 |
---|---|
interrupt() | 일시정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외처리 코드(catch)에서 실행대기 상태로 가거나 종료 상태로 갈수있도록 함. |
notify(),notifyAll() | 동기화 블록 내에서 wait() 메소드에 의해 일시정지 상태에 있는 스레드를 실행대기 상태로 만듬 |
sleep(long millis) | 주어진 시간동안 스레드를 일시정지 상태로 만들고 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. |
join() | join() 메소드를 호출한 스레드는 일시정지 상태가 됨, 실행대기 상태로 가려면 , join() 메소드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 함. |
wait() | 동기화 블록 내에서 스레드를 일시정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행대기 상태가 되며, 시간이 주어지지 않으면 notify() 메소드에 의해 실행대기 상태로 돌아갈수있음. |
yield() | 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행대기 상태가 된다. |
다른 스레드에게 실행 양보(yield())
- 스레드가 처리하는 작업은 반복적인 실행을 위해 반복문을 포함하는 경우가 많다. 가끔은 이 반복문들이 무의미한 반복을 하는 경우가 있음.
public void run(){ while(true){ if(work){ System.out.println("ThreadA 작업 내용") } else{ Thread.yield(); } } }
- work 의 값이 false 가 오기전까지 무의미한 반복을 하는데 의미없는 반복을 줄이기 위해 yield() 메소드를 호출해 실행대기 상태로 돌아가고 다른 스레드에게 실행 기회를 주도록 수정
다른 스레드의 종료를 기다림(join())
- 스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만, 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있음.
- 이런경우 Thread.join() 메소드를 사용함
public static void main(String[] args) { SumThread sumThread = new SumThread(); //해당 클래스는 100까지 합 더하는 로직 구현 sumThread.start(); try { sumThread.join(); // sumThread 가 종료할때까지 메인스레드를 일시정지 } catch(Exception e) { } System.out.println("합 : "+ sumThread.getSum()); }
- 이런경우 Thread.join() 메소드를 사용함
스레드 간 협업(wait(), notify(), notifyAll())
- 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시정지 상태에서 풀어주고, 자신은 일시정지로 상태로 만드는것.
- 이 방법의 핵심은 공유 객체에 있음.
- 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해놓고 한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시정지 상태에있는 다른 스레드를 실행대기 상태로 만들고 자신은 두번 작업을 하지 않도록 wait() 메소드를 호출하여 일시정지 상태로 만듬
- 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있음.
- 이 방법의 핵심은 공유 객체에 있음.
public class WorkObject {
public synchronized void methodA() {
System.out.println("ThreadA 의 methodA() 작업 실행");
notify(); // 일시정지 되어있는 ThreadB 를 실행대기 상태로 만듬
try {
wait(); // ThreadA 를 일시정지 상태로 만듬
}catch(Exception e) {
}
}
public synchronized void methodB() {
System.out.println("ThreadB 의 methodB() 작업 실행");
notify();
try {
wait(); //ThreadB 일시정지
} catch (Exception e) {
// TODO: handle exception
}
}
}
스레드의 안전한 종료(stop 플래그, interrupt())
- 스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료됨.
- 하지만 경우에 따라 중간에 종료시켜야 할 경우도 있음.
stop 플래그를 이용하는 방법
- 스레드는 run() 메소드가 끝나면 자동적으로 종료되므로, run() 메소드가 정상적으로 종료되도록 유도하는것이 최선
public class XXXThread extends Thread{ private boolean stop; // stop 플래그 public void run(){ while(!stop){ //stop 이 true 가 되면 run()이 종료된다. 스레드가 반복하는 실행 코드; } //스레드가 사용한 자원 정리 } }
interrupt() 메소드를 이용하는방법
- interupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할
public class InterrputExample {
public static void main(String[] args) {
Thread thread = new PrintThread2();
thread.start(); // 스레드 시작
try {
Thread.sleep(1); // 스레드 일시정지
} catch (Exception e) {
System.out.println("스레드 종료");
}
thread.interrupt(); // 스레드 종료
}
}
- 스레드가 실행대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시정지 상태가 되면 예외가 발생함.
- 스레드가 일시 정지 상태가 되지않으면 interrupt() 메소드 호출은 아무런 의미가 없음.(위에 코드에서는 일시정지 시키기 위해 Thread.sleep(1) 코드 사용)
- 일시정지를 만들지 않고도 종료되었는지 확인 하는 방법은 interrupt() 메소드가 호출 되었다면 스레드의 interrupted() 와 isInterrupted() 메소드는 true 를 리턴한다.
- interrupted()는 정적 메소드로 현재 스레드가 interrupted 되었는지 확인할 때 사용
- isInterrupted()는 인스턴스 메소드로 현재 스레드가 interrupted 되었는지 확인할 때 사용
boolean status = Thread.interrupted(); boolean status = objThread.isInterrupted();
데몬 스레드
- 데몬(daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
- 보조 역할을 수행하므로 주 스레드가 종료되면 데몬스레드도 강제적으로 자동 종료됨.
public static void main(String[] args){
AutoSaveThread thread = new AutoSaveThread(); //AutoSaveThread 가 데몬스레드가 됨.
thread.setDaemon(true); // 스레드 호출 전 선언해야 예외 발생하지않음.
thread.start();
}
- 스레드 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true) 를 호출해주면 됨.
스레드 그룹
- 스레드 그룹(ThreadGroup)은 관련된 스레드를 묶어서 관리할 목적으로 이용
- 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드와 같은 스레드 그룹에 속함.
스레드 그룹 이름얻기
ThreadGroup group = Thread.currentThread().getThreadGroup();
String groupName = group.getName();
- Thread 의 정적 메소드인 getAllStackTraces() 를 이용하면 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻음
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
스레드 그룹 생성
- 명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 ThreadGroup 객체를 만들면 됨.
ThreadGroup tg = new ThreadGroup(String name); ThreadGroup th = new ThreadGroup(ThreadGroup parent, String name);
- 스레드 그룹 생성 시 부모(Parent) 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성됨.
스레드 그룹의 일괄 interrupt()
-
스레드 그룹에서 제공하는 interrupt() 메소드를 이용 시 그룹 내 에 포함된 모든 스레드들을 일괄 interrupt 할수있음.
-
ThreadGroup 이 가지고 있는 주요메소드
메소드 | 설명 |
---|---|
int / activeCount() | 현재 그룹 및 하위 그룹에서 활동중인 모든 스레드의 수를 리턴 |
int / activeGroupCount() | 현재 그룹에서 활동 중인 모든 하위 그룹의 수를 리턴 |
void / checkAccess() | 현재 스레드가 스레드 그룹을 변경할 권한이 있는지 체크한다. 권한이 없으면 SecurityException 을 발생시킴 |
void / destroy() | 현재 그룹 및 하위 그룹을 모두 삭제한다. 단 그룹 내에 포함된 모든 스레드들이 종료 상태가 되어야 함. |
boolean / isDestroy() | 현재 그룹이 삭제 되었는지 여부를 리턴 |
int / getMaxPriority() | 현재 그룹에 포함된 스레드가 가질 수 이씨는 최대 우선순위를 리턴함. |
void / setMaxPriority(int pri) | 현재 그룹에 포함된 스레드가 가질 수 있는 최대 우선순위를 설정 |
String / getName() | 현재 그룹의 이름을 리턴 |
ThreadGroup / getParent() | 현재 그룹의 부모 그룹을 리턴 |
boolean / parentOf(ThreadGroup g) | 현재 그룹이 매개값으로 지정한 스레드 그룹의 부모인지 여부를 리턴 |
boolean / isDaemon() | 현재 그룹이 데몬 그룹인지 여부를 리턴 |
void / setDaemon(boolean daemon) | 현재 그룹을 데몬 그룹으로 설정 |
void / list() | 현재 그룹에 포함된 스레드와 하위 그룹에 대한 정보를 출력 |
void / interrupt() | 현재 그룹에 포함된 모든 스레드들을 interrupt |
스레드풀(ExecutorService)
-
갑작스러운 병렬 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀(ThreadPool) 을 사용
- 스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리함.
- 작업 처리가 끝난 스레드는 작업 결과를 애플리케이션으로 전달하고, 다시 작업큐에서 새로운 작업을 가져와 처리함.
-
Executors 클래스의 다양한 정적 메소드를 이용해서 ExecutorService 구현 객체를 만드는데 이것이 스레드 풀임
스레드 풀 생성 및 종료
스레드 풀 생성
메소드명(매개변수) | 초기 스레드 수 | 코어 스레드 수 | 최대 스레드 수 |
---|---|---|---|
newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int nThreads) | 0 | nThreads | nThreads |
- 초기 스레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수
- 코어 스레드 수 : 스레드 수가 증가 된 후 사용되지 않는 스레드를 스레드 풀에서 제거 할 때 최소한 유지해야 할 스레드 수
- 최대 스레드 수 : 스레드 풀에서 관리하는 최대 스레드 수
- newCachedThreadPool()
- 초기 스레드 개수와 코어 스레드 개수는 0개, 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시켜 작업을 처리, 이론적으로는 int 값이 가질수 있는 최대값만큼 스레드가 추가되지만, 성능에 따라 달라짐, 1개 이상의 스레드를 생성 후 60초 동안 스레드가 아무작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거
// newCachedThreadPool()을 호출해 ExecutorService 구현 객체를 얻는 코드
ExecutorService executorService = Executors.newCachedThreadPool();
- newFixedThreadPool(int nThreads)
- 초기 스레드 개수는 0개, 코어 스레드 수는 nThreads, 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시키고 작업을 처리, 최대 스레드 수는 매개값으로 준 nThreads 이다. 이 스레드 풀은 스레드가 작업을 처리하지 않고 있더라도 스레드 개수가 줄지 않는다.
// cpu 코어의 수만큼 최대 스레드를 사용하는 스레드풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
- ThreadPoolExecutor
- 코어 스레드 개수와 최대 스레드 개수를 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성, 위 두가지 메소드도 내부적으로 ThreadPoolExecutor 객체를 생성해서 리턴함.
ExecutorService threadPool = new ThreadPoolExecutor( 3, //코어 스레드 개수 100, // 최대 스레드 개수 120L, // 놀고 있는 시간 TimeUnit.SECONDS, //놀고있는 시간 단위 new SynchronousQueue<Runnable>() //작업 큐 );
- 코어 스레드 개수와 최대 스레드 개수를 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성, 위 두가지 메소드도 내부적으로 ThreadPoolExecutor 객체를 생성해서 리턴함.
스레드풀 종료
- 스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료 되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있음.
리턴타입 | 메소드명(매개변수) | 설명 |
---|---|---|
void | shutdown() | 현재 처리중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료 시킴. |
List’<’Runnable’>’ | shutdownNow() | 현재 작업 처리중인 스레드를 interrupt 해서 작업 중지를 시도하고 스레드 풀을 종료시킴, 리턴값은 작업큐에 있는 미처리된 작업(Runnable)의 목록 |
boolean | awaitTermination(long timeout, TimeUnit unit) | shutdown() 메소드 호출 이후, 모든 작업 처리를 timeout 시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리중인 스레드를 interrupt 하고 false 를 리턴함. |
executorService.shutdown(); // 작업을 마무리하고 스레드풀 종료 시 사용
또는
executorService.shutdownNow(); // 작업과 상관없이 강제 종료 시 사용
작업 생성과 처리 요청
작업 생성
- 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현함.
- Runnalbe 과 Callable 의 차이점은 작업처리 완료 후 리턴값이 있느냐 없느냐임.
Runnable task = new Runnable(){
@Override
public void run(){
//스레드 처리 내용
}
}
Callable<T> task = new Callable<T>(){
@Override
public T call() throws Exception{
//스레드 처리 내용
return T;
}
}
작업 처리 요청
- 작업 처리 요청이란 ExecutorService 의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위
리턴타입 | 메소드명(매개변수) | 설명 |
---|---|---|
void | execute(Runnable command) | Runnable을 작업 큐에 저장, 작업 처리 결과를 받지 못함 |
Future<?>, Future<’V’>,Future<’V’> | submit(Runnable task), submit(Runnable task, V result), submit(Callable<’V’> task) | Runnable 또는 Callable을 작업 큐에 저장, 리턴된 Future 를 통해 작업 처리 결과를 얻을 수 있음. |
- 두 메소드의 차이점
- execute() 는 작업 처리 결과를 받지 못함
- submit() 은 작업처리 결과를 받을수 있도록 Future 를 리턴
- execute() 는 작업처리 도중 예외가 발생하면 스레드가 종료되고, 해당 스레드는 스레드 풀에서 제거되고 다른 작업 처리를 위해 새로운 스레드 생성
- submit() 은 작업 처리 도중 예외가 발생하더라도 스레드는 종료되지 않고 다음작업을 위해 재사용됨.
블로킹 방식의 작업 완료 통보
-
ExecutorService 의 submit() 메소드는 매개값으로 준 Runnable 또는 Callable 작업을 스레드 풀의 작업큐에 저장하고 즉시 Future 객체를 리턴함.
- Future 객체는 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가(지연했다가=블로킹되었다가) 최종 결과를 얻는데 사용
Future를 지연 완료(pending completion) 객체라고 함.
- Future 의 get() 메소드를 호출하면 스레드가 작업을 완료할 때가지 블로킹되었다가 작업을 완료하면 처리 결과를 리턴함.
- 블로킹을 사용하는 작업 오나료 통보 방식
리턴타입 | 메소드명(매개변수) | 설명 |
---|---|---|
V | get() | 작업이 완료될 때까지 블로킹되었다가 처리 결과 V를 리턴 |
V | get(long timeout, TimeUnit unit) | timeout 시간 전에 작업이 완료되면 결과 V를 리턴하지만, 작업이 완료되지 않으면 TimeoutException 을 발생 시킴 |
- submit() 메소드별로 Future 의 get() 메소드가 리턴하는값
메소드 | 작업처리 완료 후 리턴 타입 | 작업처리 도중 예외 발생 |
---|---|---|
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() 메소드가 블로킹 되므로 다른 코드를 실행할 수 없음.
//새로운 스레드를 생성해서 호출
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.printStackTrack();
}
}
});
리턴값이 없는 작업 완료 통보
- 리턴값이 없는 작업일 경우에는 Runnable 객체로 생성
Runnable task = new Runnable(){
@Override
public void run(){
//스레드 작업 처리내용
}
};
- 결과값이 없는 작업 처리 요청은 submit(Runnable task)메소드를 이용하면 됨.
Future future = executorService.submit(task);
try{
future.get();
} catch(InterruptedException e){
//작업 처리 중 스레드가 interrupt 될 경우 실행될 코드
} catch(ExecutionException e){
//작업 처리 중 예외 발생 된 경우 실행할 코드
}
- 결과값이 없음에도 Future 객체를 리턴하는데 이것은 스레드가 작업 처리를 정상적으로 완료했는지 예외가 발생했느지 확인하기 위해서임. (완료되었다면 null, 예외 발생 시 Exec0 리턴값이 있는 작업 완료 통보
- 스레드풀의 스렏드가 작업을 완료 후 애플리케이션이 처리 결과를 얻어야 한다면 작업 객체를 Callable 로 생성
Callable<T> task = new Callable<T>(){
@Override
public T call() throws Exception{
//스레드가 처리할 작업 내용
return T;
}
};
- sbumit() 메소드는 작업 큐에 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){
//작업 처리 중 예외 발생 된 경우 실행할 코드
}
작업 처리 결과를 외부 객체에 저장
- 두개 이상의 스레드 작업을 취합할 목적으로 ExecutorService 의 submit(Runnable task, V result) 메소드를 사용
// 공유 객체 스레드가 공통적으로 사용하는 객체 생성
Result result = ...;
// 작업 객체 안에 공용 객체를 넣어둠(result), 스레드가 작업 후 처리결과를 누적시키려면 참조를 알고있어야 하므로 넣어둠
Runnable task = new Task(result);
// submit 호출 시 작업 객체, 공유 객체를 넣어둠, 그러면 Future 를 리턴함 <Result> 도 공유 객체와 타입 동일해야함.
Future<Result> future = executorService.submit(task, result);
//submit 에 저장된 공유 객체를 리턴함.
result = future.get();
작업 완료 순으로 통보
- CompletionServices 를 이용해 작업 처리가 완료된것만 통보 받음.
리턴타입 | 메소드명(매개 변수) | 설명 |
---|---|---|
Future<’V> | poll() | 완료된작업의 Future를 가져옴, 완료된 작업이 없다면 null 을 리턴함. |
Future<’V> | poll(long imteout, TimeUnit unit) | 완료된작업의 Future를 가져옴, 완료된 작업이 없다면 timeout 까지 블로킹 됨. |
Future<’V> | take() | 완료된작업의 Future를 가져옴, 완료된 작업이 없다면 있을때까지 블로킹 됨. |
Future<’V> | submit(Callable<’V>)task | 스레드풀에 Callable 작업 처리 요청 |
Future<’V> | submit(Runnable task, V result) | 스레드풀에 Runnable 작업 처리 요청 |
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //참조타입 변수 선언
CompletionService<V> completionService = new ExecutorCompletionService<V>(executorService); // 매개변수로 참조 타입 넣을것
- poll()과 take() 메소드를 이용해서 처리완료된 작업의 Future 를 얻기 위해선 CompletionService 의 submit()메소드로 작업 처리 요청 해야함.
completionService.submit(Callable<V> task);
completionService.submit(Runnable task, V result);
- take() 메소드를 호출하여 완료된 Callable 작업이 있을때까지 블로킹 되었다가 완료된 작업의 Future 를 얻고, get() 메소드로 결과값을 얻어내는 코드
executorService.submit(new Runnable(){
@Override
public void run(){
while(ture){
try{
Future<Integer> future = completionService.take(); // 완료되느 작업이 있을때까지 블로킹, 완료된 작업이 있으면 Future 를 리턴
int value = future.get(); // get()은 블로킹 되지 않고 바로 작업 결과를 리턴
System.out.println("처리결과" + value);
} catch(Exception e){
break;
}
}
}
});
콜백 방식의 작업 완료 통보
- 콜백이란 애플리케이션이 스레드에게 작업 처리를 요청한 후 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법
- 이때 자동 실행되는 메소드를 콜백 메소드라고 함.
- 블로킹 방식은 작업 처리를 요청한 후 작업이 완료될때까지 블로킹 되지만, 콜백 방식은 작업처리를 요청 한 후 결과를 기다릴 필요없이 다른 기능을 수행
- Runnable 구현 클래스를 작성 시 콜백 기능을 구현 가능함.
- 먼저 콜백 메소드를 가진 클래스가 있어야 하는데, 직접 정의해도 되고, java.nio.channels.CompletionHandler 를 이용해도 됨.
CompletionHandler<V, A> callback = new ComplteionHandler<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 로 지정