Java Standard - 쓰레드(Thread)
프로세스와 쓰레드의 차이 구분해보자.
프로세스와 쓰레드
프로세스: 실행 중인 프로그램, 비유 - 작업공간
프로그램 실행시 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 됨
- 구성: 자원(데이터나 메모리 등), 쓰레드
쓰레드: 프로세스의 자원을 이용해 실제로 작업을 수행하는 것, 비유 - 일꾼
멀티태스킹과 멀티쓰레드
- 멀티태스킹: 다중작업
멀티쓰레드: 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것
CPU의 코어는 한번에 단 한개의 일만 할 수 있음
- 장점
- CPU의 사용률을 향상시킴
- 자원을 보다 효율적으로 사용할 수 있음
- 사용자에 대한 응답성이 향상
- 작업이 분리되어 코드가 간결해짐
- 문제점
- 동기화
- 교착상태: 두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰진 상태
- 장점
쓰레드의 구현과 실행
쓰레드의 구현
Thread 클래스를 상속받는 방법(다른 클래스를 상속받을 수 없음)
1
2
3
4
5
6
7
8
9
10
11
12
class MyThread extends Thread{
public void run(){
//작업 내용 작성
}
}
class ThreadEx{
public static void main(String args[]){
Thread t = new MyThread();
t.start();
}
}
Runnable 인터페이스를 구현하는 방법: 일반적인 방법, 재사용성이 높고, 코드의 일관성을 유지 할 수있음
- Runnable 인터페이스는 run()만 정의되어있는 간단한 인터페이스
1
2
3
4
5
6
7
8
9
10
11
12
class MyThread implements Runnable{
public void run(){
//작업 내용 작성
}
}
class ThreadEx{
public static void main(String args[]){
Runnable r = new MyThread();
Thread t = new Thread(r);//생성자에 Runnable을 구현한 클래스를 넣어줌
t.start();
}
}
Thread 메서드
- 메서드
- currentThread(): 현재 실행중인 쓰레드의 참조를 반환
- getName(): 쓰레드의 이름을 반환
- start(): 쓰레드의 실행
- 쓰레드 접근
- thread 상속: 바로 접근하면 됌
- Runnable 구현 방식: 접근할 수 없음, static메서드로만 알 수 있음
쓰레드의 실행
start() 메서드를 호출해야지만 쓰레드가 실행됨
- start() 메서드 실행시 동작 내용
- 실행대기 상태
- 자신의 차례가 되면 동작
- 하나의 쓰레드에 대해 start()가 한번만 호출 될 수 있음
- 쓰레드의 실행 순서는 OS의 스케쥴러가 작성한 스케쥴에 의해 결정됨
start()와 run()
start()와 run()의 차이
- run()의 의미: 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것 뿐
- start()
쓰레드와 호출스택 모든 쓰레드는 독립적인 작업을 하기 위해 자신만의 호출스택을 필요로 함 새로운 쓰레드를 생성할 때 마다 새로운 호출스택이 생성됨 쓰레드가 종료되면 호출스택도 소멸됨
우선순위 쓰레드가 둘 이상일 때는 호출스택의 최상위에 있더라도 대기상태일 수 있음 자신의 순서가되면 일정시간만 작업을 함
main 쓰레드
main메서드의 작업을 수행하는 것도 쓰레드
- 프로그램의 동작방식
- 프로그램을 실행하면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 작업을 수행함
- 실행 중인 사용자 쓰레드가 하나도 없으면 프로그램은 종료됨(main 쓰레드가 아니여도)
- 쓰레드의 종류
- 사용자 쓰레드(non daemon thread)
- 데몬 쓰레드
쓰레드가 실행되는 과정
- run() 호출
- 단순히 main 호출 스택에서 run()메서드가 실행되는 것 뿐
- run()이 끝날때까지 main 호출스택이 끝나지 않음
- start() 호출:
- 해당 쓰레드의 호출 스택이 생성되고 run()을 호출
- main 호출스택이 다른 일이 있는게 아니라면 끝나고 쓰레드의 호출스택만 run()이 끝날때까지 남음
싱글쓰레드와 멀티쓰레드
- 컨텍스트 스위칭으로 인한 시간 소요로 인해 멀티 쓰레드가 시간이 더 걸림
컨텍스트 스위칭 프로세스 또는 쓰레드 간의 작업 전환 현재 진행 중인 작업의 상태등의 정보를 저장하고 읽어 오는 것 쓰레드 스위칭 보다 프로세스 스위칭이 더 많은 정보를 저장해야해서 많은 시간이 걸림
싱글코어의 멀티쓰레드와 멀티코어의 멀티쓰레드의 차이 한 화면에 두개의 쓰레드가 출력하는 예제
- 멀티코어의 멀티쓰레드의 시간이 오래걸린 원인
- 컨텍스트 스위칭의 시간소요
- 출력하는 화면은 한개뿐이라, 출력이 끝나기를 기다려야하는 대기시간 → 같은 자원(화면)을 놓고 경쟁하게 됨
- 싱글코어의 멀티쓰레드는 작업이 절대 겹치지 않음 → 경쟁이 없음
- 병행과 병렬
- 병행
- 여러 쓰레드가 여러 작업을 동시에 진행하는 것
- 병렬
- 하나의 작업을 여러 쓰레드가 나눠서 처리하는 것
OS 스케쥴러 JVM이 OS에 독립적이지만 몇개는 종속적, 쓰레드도 그 중 하나 쓰레드는 OS의 프로세스 스케줄러의 영향을 받음
- 어떤 쓰레드가 실행될 것인지
- 얼마나 실행될 것인지 등
효율적인 쓰레드 작업
두 쓰레드가 서로 다른 자원을 사용하는 작업이라면 멀티 프로세스가 더욱 효과적
- 첫번째 케이스는 사용자로부터 입력을 기다리는 동안 다른 작업을 하지 못함
- 두번째 케이스는 사용자로부터 입력을 기다리는 동안 다른 작업을 할 수 있어 효율적인 CPU 사용이 가능함
쓰레드의 우선순위
특징
- 쓰레드는 우선순위라는 멤버변수를 가지고 있음
- 우선순위 값에 따라 쓰레드가 얻는 실행시간이 달라짐
- 우선순위를 서로 다르게 지정해 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있음
우선순위 지정하기
- 메서드
- setPriority(int newPriority): 쓰레드의 우선순위를 지정한 값으로 변경
- getPriority(): 쓰레드의 우선순위를 반환
- 상수
- MAX_PRIORITY = 10: 최대 우선순위
- MIN_PRIORITY = 1: 최소 우선순위
- NORM_PRIORITY = 5: 평균 우선순위
- 특징
- 우선순위를 지정한 다음에 쓰레드를 시작(start())해야함
- 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받음
- main에서 생성하는 쓰레드는 우선순위를 지정하지 않으면 5 값으로 지정됨
싱글코어와 멀티코어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class PriorityThread{
public static void main(String arg[]){
Thread_5 th1 = new Thread_5();
Thread_7 th2 = new Thread_7();
th2.setPriority(5);
System.out.println("Priority of th1(-) : "+ th1.getPriority());
System.out.println("Priority of th2(-) : "+ th2.getPriority());
th1.start();
th2.start();
}
}
class Thread_5{
public void run(){
for(int i=0; i<300;i++){
System.out.println("-");
for(int x=0; x<1000000000;x++);
}
}
}
class Thread_7{
public void run(){
for(int i=0; i<300;i++){
System.out.println("|");
for(int x=0; x<1000000000;x++);
}
}
}
- 우선순위가 같으면 같은 양의 실행시간이 주어짐
- 우선순위가 다르면 우선순위가 높은 쓰레드에게 상대적으로 더 많은 양의 실행시간이 주어짐
멀티코어
- 쓰레드의 우선순위로 인한 차이가 없음
- 하지만 OS마다 다른 방식으로 스케쥴링 하기 때문에, OS의 스케쥴링 정책과 JVM의 구현을 직접 확인해봐야함 → 예측만 가능
- 이 때문에 쓰레드의 우선순위가 아닌 작업의 우선순위를 두어 PriorityQueue에 저장한뒤 우선순위가 높은 작업 먼저 처리되도록 하는 것이 나을 수 있음
쓰레드 그룹(thread group)
정의: 서로 관련된 쓰레드를 그룹으로 다루기 위한 것
특징
- 쓰레드 그룹을 생성해 쓰레드를 그룹으로 묶어서 관리할 수 있음
- 쓰레드 그룹에 다른 그룹을 포함 시킬 수 있음
- 자신이 속한 쓰레드 그룹, 하위 쓰레드 그룹만 변경 가능
- 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어야함, 미지정시 자신을 생성한 쓰레드와 같은 그룹에 속함
- 보안상의 이유로 도입된 개념
- 쓰레드를 쓰레드 그룹에 포함시키기 위해서는 Thread의 생성자를 이용해야함
JVM의 쓰레드 그룹
- main 쓰레드 그룹: main 쓰레드 등
- main에서 생성된 쓰레드를 쓰레드 그룹을 지정하지 않으면 main 쓰레드 그룹으로 지정됨
- system 쓰레드 그룹: Finalizer 쓰레드(가비지 컬렉션) 등
- getThreadGroup(): 쓰레드 자신이 속한 그룹을 반환
- uncaughtException(): 쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행이 종료되었을때, JVM에 의해 자동으로 호출되는 메서드
데몬 쓰레드(daemon thread)
의미: 다른 일반쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
특징
- 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 종료, 일반쓰레드를 돕는 쓰레드인데 일반쓰레드가 종료된 이후에는 의미가 없기 때문
- 데몬 쓰레드가 생성한 쓰레드도 데몬쓰레드
- 다양한 데몬 쓰레드가 존재(GC, 이벤트나 그래픽 처리 등), 이들은 system 쓰레드 그룹이나 main쓰레드 그룹에 속함
작성 방법
- 무한루프와 조건문을 이용(특정 조건시에만 작업수행 후 대기)
실행 방법
- 쓰레드 실행전에 setDaemon(true) 호출
메서드
- isDaemon(): 쓰레드가 데몬인지 확인
- setDaemon(): 쓰레드를 데몬쓰레드 혹은 사용자 쓰레드로 변경
호출스택 조회 getAllStackTraces() 이용시 작업이 완료되지 않은 모든 쓰레드의 호출스택을 조회 가능
쓰레드의 실행제어
쓰레드 프로그래밍이 어려운이유: 동기화, 스케줄링
스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련된 메서드를 잘 알아야함
쓰레드의 상태
deprecated된 메서드: resume()
, stop()
, suspend()
- 쓰레드를 교착상태로 만들기 쉽기 때문
- 쓰레드 생성
start()
호출: 바로 실행되는 것이 아니라 실행대기열에 저장실행대기열 큐(queue)와 같은 구조로 먼저 들어오면 먼저 실행됨
- 실행대기열에서 자신의 차례가 되면 실행상태가 됨
실행중에 suspend(), wait(), join(),I/O block에 의해 일시정지 상태가 될 수 있음
I/O block 입출력작업에서 발생하는 지연상태 예를 들어 사용자의 입력을 기다리는 경우를 말하는데, 이런경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 됨
- time-out(지정된 시간이 다됨),
notify()
,resume()
,interrupt()
가 호출되면 일시정시 상태를 벗어나 실행대기열에 저장되어 자신의 차례를 기다림 - 실행을 모두 마치거나 stop() 호출이 되어 소멸
sleep()
- 일정시간동안 쓰레드를 멈춤
1
2
static void sleep(long millis)
static void sleep(long millis, int nanos)
특징
- 특정 시간을 지정할 수 있지만, 어느정도 오차가 발생할 수 있음
InterruptedException
발생할 수 있기 때문에try-catch문
으로 예외처리를 해야함- 항상 현재 실행 중인 쓰레드에 대해 작동, 참조변수를 이용한 호출 보다는 해당 쓰레드 코드 내에서 작성되어야함
실행대기가 되는 조건
- 지정된 시간이 다 지남
interrupt()
가 호출되어InterruptedException
발생으로 실행대기 상대가 됨
interrupt()
와 interrupted()
- 쓰레드의 작업을 취소
메서드
void interrupt()
: 쓰레드의 interrupted 상태를 false → true 변경 쓰레드에게 작업을 멈추라고 요청, 쓰레드를 강제로 종료하지는 못함static boolean isInterripted()
: 현재 쓰레드의 interrupted 상태를 반환boolean interrupted()
: 쓰레드의 interrupted상태를 반환 후, false로 변경 쓰레드에 대해interrupt()
가 호출되었는지 알려줌
이용한 예제
1
2
3
4
5
6
7
8
9
10
11
12
Thread th = new Thread();
th.start();
...
th.interrupt(); // th의 interrupt()를 호출
class MyThread extends Thread{
public void run(){
while(!interrupted()){ //interrupted()의 결과과 false인 동안 반복
...
}
}
}
특징
sleep()
에 의해 쓰레드가 잠시 멈춰있을때 interrupt()
를 호출하면, InterruptedException이 발생되고, 쓰레드의 Interrupted상태를 자동으로 false로 초기화 됨
따라서 이때는 catch 블럭에서 또 interrupt()
를 호출해 Interrupted 상태를 true로 변경해야함
suspend()
, resume()
, stop()
의미
- suspend(): 쓰레드를 멈추게함
- resume(): suspend()에 의해 멈춰진 쓰레드를 다시 실행대기 상태로 바꿈
- stop(): 쓰레드를 즉시 종료시킴
⇒ 셋 다 deprecated, 교착상태를 쉽게 일으킬 수 있기 때문에
대체 방법
- suspend()와 resume(): 인스턴스 변수로 선언해 if문으로 대체, 잘 안되면 volatile추가
- stop(): 인스턴스변수로 선언해 while문으로 대체, 잘 안되면 volatile추가
yield()
- 다른 쓰레드에게 양보
의미
- 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보함
- ex) 1초 할당, 0.5초 작업후 yield() 호출시 실행대기 상태가 됨
특징
바쁜 대기상태로 있지 않고 다른 쓰레드에게 실행 시간을 양보함으로써 더 효율적
바쁜 대기상태 왼쪽코드에서 suspended 값이 true일때(=실행을 잠시 멈춘 상태일때)라면, 쓰레드는 주어진 실행 시간을 그저 while문을 의미 없이 돌며 낭비하게됨 이런 상태를 바쁜 대기상태 라고함
join()
- 다른 쓰레드의 작업을 기다림
의미
1
2
3
void join()
void join(long millis)
void join(long millis, int nanos)
- 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 join()을 사용
- 다른 쓰레드의 작업이 먼저 수행되어야할 때 사용
- 해당 쓰레드가 작업을 모두 마칠때까지 기다리게됨
특징
interrupt()
에 의해 대기상태에서 벗어날 수 있음try-catch문
으로 감싸 예외처리를 해줘야함- static 메서드가 아니라
sleep()
과 달리 특정 쓰레드에 대해 동작함
쓰레드의 동기화
- 필요성: 여러쓰레드가 같은 프로세스 내에서 자원을 공유해 작업하기 때문에 서로의 작업에 영향을 주게됨
- 쓰레드의 동기화
- 한쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것
- 임계영역
- 공유데이터를 사용하는 코드영역
- 잠금
- lock을 획득한 단 한개의 쓰레드만 임계영역에 접근해 작업을 진행 할 수 있고, 작업을 마친 쓰레드는 lock을 반납해 다른 쓰레드가 임계영역에 접근할 수 있도록 함
동기화 지원 방법
- synchronized 블럭
- java.util.concurrent.locks
- java.util.concurrent.atomic 패키지
synchronized를 이용한 동기화
- 의미: 임계 영역을 설정하는데 사용됨
방식
메서드 전체를 임계 영역으로 지정
작성 방법: 메서드 앞에
synchronized
를 붙임1 2 3
public synchronized void calcSum(){ //... }
lock의 흐름 메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행 메서드 종료시 lock 반환
특정한 영역을 임계 영역으로 지정 작성방법: 코드 일부를 블럭
{}
으로 감싸고 블럭 앞에synchronized(참조변수)
를 붙임이때 참조변수는 락을 걸고자하는 객체를 참조하는 것
1 2 3
synchronized(객체의 참조변수) { //... }
lock의 흐름 블럭 안으로 들어가면서 지정된 객체의 lock을 얻음 블럭 밖으로 벗어나면서 lock을 반납
특징
- lock의 획득과 반납이 자동으로 이루어짐, 임계 영역만 설정하면 됨
- 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우 → 임계영역을 최소화 해야함
예제(출금)
메서드 호출시 lock을 얻지 못하면 wait이 아니라 그냥 넘어가는건지?!!
wait()과 notify()
lock의 문제상황
특정 쓰레드가 객체의 lock을 가지고 오랜 시간을 보내는 것
→ 개선하기 위해
wait()
과notify()
가 고안됨- 의미
wait()
: 쓰레드가 lock을 반납하고 해당 객체의 대기실(waiting pool)에서 기다리게함
해당 객체의 대기실(waiting pool) 객체마다 존재하는 것으로 notifyAll()이 호출된다고해서 모든 객체의 waiting pool에 있는 쓰레드가 깨워지는 것이 아니라 해당 객체에 있는 쓰레드만 해당됨
notify()
: 특정 기다리던 쓰레드에게 다시 lock을 주겠다고 통지함notifyAll()
: 모든 쓰레드에게 다시 lock을 주겠다고 통지함- 문제점
- 오래 기다린 쓰레드가 다시 lock을 얻는다는 보장이 없다는 것
notify()
나notifyAll()
는 lock을 주는 것이 아니라 주겠다고 통지하는 것- lock을 얻지 못하면 다시 기다려야하는 신세
- 특징
- Object클래스에 정의되어 있음
- 실행 중이던 쓰레드는 해당 객체의 대기실에서 통지를 기다림
- synchronized블록내에서만 사용할 수 있음
- 예제(손님과 주방장)
왜 wait()을 이용해 cook쓰레드를 기다리게 하는지? 왜 notify()로 기다리고 있는 cust를 깨우는지??? 음식을 지우고 cook을 깨우는지??
기아현상과 경쟁 상태
- 기아현상: 쓰레드가 계속 통지를 받지 못하고 오랫동안 기다리게 되는 상태
- 현상을 막기 위해서는 notify() 대신 notifyAll()을 사용해야함
- 경쟁상태: 여러 쓰레드가 lock을 얻기 위해 서로 경쟁하는 것
- 경쟁상태를 개선하기 위해서는 Lock과 Condition을 이용해야함
Lock과 Condition을 이용한 동기화
Lock 클래스
ReentrantLock
의미
- 재진입이 가능한 lock
- 일반적인 lock
- 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와 이후 작업을 수행할 수 있기 때문
특징
- 가장 오래 기다린 쓰레드가 lock을 획득할 수 있게 설정할 수 있음 → 성능이 떨어짐
- 수동으로 lock을 잠구고 해지해야함 → 임계 영역에서 잠긴 상태로 예외 발생, return 시 lock이 안풀림 ⇒
try-finally문으로 감싸서 처리하는 것이 일반적
메서드
void lock()
: lock을 잠굼void unlock()
: lock을 해지boolean isLocked()
: lock이 잠겼는지 확인boolean tryLock()
: lock이 걸려있으면, lock을 얻으려고 기다리지 않음boolean tryLock(long timeout, TimeUnit unit)
: lock이 걸려있으면, lock을 얻으려고 일정시간만큼만 기다림
ReentrantReadWriteLock
의미
- 읽기에는 공유적, 쓰기에는 배타적인 Lock
- 읽기를 위한 lock, 쓰기를 위한 lock을 제공
- 읽기를 위한 lock끼리만 중복가능, 그 외에는 불가능
StampedLock
의미
- ReentrantReadWriteLock에 낙관적인Lock 추가
- 낙관적인 lock: 쓰기 lock에 의해 자동으로 풀리는 lock
- 값 읽기를 할 때는 낙관적 읽기 lock을 건 다음 읽기와 쓰기가 충돌되어 낙관적 읽기가 풀렸을 때만, 쓰기가 끝난 후에 읽기 lock을 거는 것
- lock을 걸거나 해지할 때 스탬프(long타입의 정수값)를 사용
예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int getBalance(){
long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 검
int curBalance = this.balance; // 공유 데이터 balance를 읽음
if(!lock.validate(stamp)){ // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
stamp = lock.readLock(); // 풀렸으면, 읽기lock을 얻으려고 기다림
try{
curBalance = this.balance;//공유 데이터를 다시 읽음
}finally{
lock.unlockRead(stamp);// 읽기 lock을 품
}
}
}
Condition
쓰레드를 구분해서 통지하지 못한 문제점을 해결하기 위해 나온 클래스
- 생성 방법
- 생성된 lock으로부터 생성함
1
2
3
4
5
private ReentrantLock lock = new ReentrantLock(); //lock 생성
//lock으로 condition 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
메서드
await()
:wait()
대신 사용하는 메서드signal()
:notify()
대신 사용하는 메서드
특징
- 이전에 wait()과 notify()를 사용할 때보다는 기아현상과 경쟁 상태가 개선됨
- 특정 쓰레드를 공유 객체의 waiting pool이 아닌 커스텀한 Condition의 waiting pool에서 따로 기다리게 할 수 있음
- 하지만 역시 특정 쓰레드를 선택해 통지하는 것은 불가능
volatile
메모리에서 읽어온 값을 캐시에 저장 → 캐시에서 값을 읽어서 작업, 값이 캐시에 없을 때만 메모리에서 읽어옴
⇒ 메모리와 캐시의 값 불일치: 도중에 메모리에 저장된 변수의 값이 변경되어도 캐시에 저장된 값이 갱신되지 않아 메모리와 캐시에 저장된 값이 다른 경우가 생김
volatile
의미
변수앞에 volatile을 붙이면 코어가 변수의 값을 읽을 때 캐시가 아닌 메모리에서 읽어옴
→ 메모리와 캐시의 값 불일치 해결(synchronized블럭으로도 가능함)
long과 double을 원자화
- 원자화를 해야하는 이유
int와 int보다 작은 타입들은 작업 중간에 다른 쓰레드가 끼어들 틈이 없음
JVM은 데이터를 4byte단위로 처리하기 때문에 하나의 명령어로 읽기 쓰기가 가능해서
long이나 double타입의 변수는 변수의 값을 읽거나 쓰는 과정에서 다른 쓰레드가 끼어들 여지가 있음
- 해결 방법: 원자화
- volatile 키워드를 이용해 변수를 원자화 하는 것
상수에는 붙일 수 없음 상수는 변할 수 없는 값이라 멀티쓰레드에 안전 → 붙일 필요가 없음
- synchronized 블럭으로 감싸서 다른 쓰레드가 끼어들지 못하게 하는 것 → 원자화로 동기화 하는 방법
원자화를 하는 것과 성능의 관계? 멀티쓰레드로 작성된 코드에서 4byte가 넘는 모든 변수를 원자화해도 성능에 문제가 없는지,,?
fork & join 프레임웍
- 의미
- 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는 것을 쉽게 만들어 줌
- 사용 방법
- 반환값이 없으면
RecursiveAction
을 상속받아compute()
구현 - 반환값이 있으면
RecursiveTask
을 상속받아compute()
구현
- 반환값이 없으면
- 동작 순서
- 작업내용을
compute()
안에 작성해 구현 invoke()
로 작업시작- 쓰레드 풀은 쓰레드가 수행해야하는 작업이 담긴 큐를 제공하며, 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리함
- 작업내용을
ForkJoinPool
- 특징
- fork & join 프레임웍에서 제공하는 쓰레드 풀
- 지정된 수의 쓰레드를 생성해서 미리 만들어놓고 반복해서 재사용 할 수 있게함
- 생성되는 쓰레드 개수는 기본적으로 코어의 개수와 같음
- 장점
- 쓰레드를 반복해서 생성하지 않아도 됨
- 너무 많은 쓰레드가 생성되어 성능이 저하되지 않음
compute()의 구현
- 구현 내용
- 수행할 작업
- 작업을 나눌 기준
예시(1~8 숫자 더하기)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Long compute(){
long size = to - from + 1: // from <= i <= to
if(size<=5) // 더할숫자가 5개 이하면
return sum(); // 숫자의 합을 반환, sum()은 from부터 to까지 수를 더해서 반환
//범위를 반으로 나눠서 두 개의 작업을 생성
long half = (from + to)/2;
sumTask leftSum = new SumTask(from, half);
sumTask rightSum = new SumTask(half+1, to);
leftSum.fork();//작업(leftSum)을 작업 큐에 넣음
return rightSum.compute() + leftSum.join();
}
- 작업의 size가 2가 될때까지 나눔
- 첫번째
compute()
호출시 더할 숫자의 범위를 반으로 나눠fork()
를 호출해 작업큐에 저장 - 하나의 쓰레드는
compute()
를 재귀호출하면서 작업을 계속해서 반으로 나눔 - 다른 쓰레드는
fork()
에 의해 작업 큐에 추가된 작업을 수행- compute()에 의해 더이상 나눌 수 없을 때까지 반복해서 나뉨
- 자신의 작업큐가 비어있는 쓰레드는 다른쓰레드의 작업큐에서 작업을 가져와 수행(=작업 훔쳐오기)
fork()와 join()
- 방식
compute()
: 작업을 나눔fork()
: 작업을 큐에 넣음, 비동기- 나눠진 작업은 각 쓰레드가 골고루 나눠서 처리
join()
: 작업의 결과 조회, 동기
- 동기와 비동기
- 동기: 수행이 끝날때까지 기다리고 결과를 얻음
- 비동기: 메서드를 호출만 할 뿐 결과를 기다리지 않고 돌아옴, 내부적으로는 다른 쓰레드에게 작업을 수행하도록 지시만 함
결론
실행결과를 보면, fork&join 프레임웍으로 계산한 결과보다 for문으로 계산한 결과가 시간이 덜 걸리는 경우도 있음
작업을 나누고 다시 합치는데 시간이 소요되기 때문
반드시 테스트해보고 이득이 있을 때에만 멀티쓰레드로 처리할 것