Java 의 Thread 는 발전을 거듭하면서 synchronized 블럭만으로는 부족했던 기능들을 ReentrantLock, Condition 을 통해 제공하게 되었습니다.
| Lock 종류 | 설명 |
|---|---|
| ReentrantLock | 재진입이 가능한 lock, 같은 객체에서 lock 을 얻었으면 그걸 계속 사용할 수 있음, 기본 lock |
| ReentrantReadWriteLock | 읽기끼리는 허용해줌, 쓰기면 lock 을 획득해야 함 |
| StampedLock | 낙관적 읽기, 일단 읽기를 시도하고 확인했더니 쓰기 lock 이었다면 lock 을 획득하고 다시 읽음 |
| Lock 메서드 | 설명 |
|---|---|
| void lock() | lock 잠금 |
| void unlock() | lock 해제 |
| boolean isLocked() | lock 잠겼는지 확인 |
| boolean tryLock() | lock 잠겼으면 기다리지 않음, lock 을 얻으면 true 반환 |
| boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException | lock 잠겼으면 지정한 시간동안만 기다림 |
| Condition newCondition() | 새로운 쓰레드 대기 영역을 생성 |
| Condition 메서드 | 설명 |
|---|---|
| void await() | 현재 스레드를 락을 해제하고 다른 스레드의 신호를 기다리는 대기 상태로 |
| void awaitUninterruptibly() | 현재 스레드를 인터럽트 없이 락을 해제하고 다른 스레드의 신호를 기다리는 대기 상태로 |
| boolean await(long time, TimeUnit unit) | 현재 스레드를 지정된 시간 동안 락을 해제하고 다른 스레드의 신호를 기다리는 대기 상태로 |
| long awaitNanos(long nanosTimeout) | 현재 스레드를 최대 나노초 동안 락을 해제하고 다른 스레드의 신호를 기다리는 대기 상태로 |
| boolean awaitUntil(Date deadline) | 현재 스레드를 특정 데드라인까지 락을 해제하고 다른 스레드의 신호를 기다리는 대기 상태로 |
| void signal() | 이 Condition 에서 대기하고 있는 단일 스레드를 깨움 |
| void signalAll() | 이 Condition 에서 대기하고 있는 모든 스레드를 깨움 |
현대의 멀티 코어 프로세서를 사용하는 컴퓨터는 CPU 마다 cache 를 가지게 되면서 메모리와 동기화 되지 않아 각 cache 마다 상이한 값을 가지게 되는 cache coherence 문제가 생기게 되었습니다.
이를 해결하기 위해 Java 에서는 volatile 키워드를 제공합니다.
✅ volatile 키워드가 붙은 변수는 반드시 메모리에서 값을 가져옵니다.
✅ long, double 과 같은 CPU 가 2개 이상의 명령어를 필요로 하는 타입의 변수를 volatile 키워드로 원자화 할 수 있습니다.
멀티 쓰레드를 사용할 수 있게 되었다고 해서 모든 것이 효율적으로 바뀐것은 아닙니다.
여전히 쓰레드 생성, 관리, 작업 분할 및 결과 병합은 골치아픈 과제입니다.
Java 는 이러한 과제들을 알아서 처리해주는 기능을 fork & join 프레임워크를 통해 제공합니다.
✅ 사용자는 작업 분할 방식만 정의하면, fork & join 프레임워크가 쓰레드 관리 및 작업 스케줄링을 알아서 처리해줍니다.
| 추상 클래스 | 설명 |
|---|---|
| RecursiveAction | 반환값이 없는 작업을 구현할 때 사용 |
| RecursiveTask | 반환값이 잇는 작업을 구현할 때 사용 |
Recursive 라는 단어가 붙은 이유는 분할 정복을 통해 재귀적으로 compute() 를 호출하게끔 compute() 를 구현하기 때문입니다.
✅ 작업을 분할해서 .fork() 를 통해 해당 작업을 쓰레드 풀의 작업 큐에 넣습니다.
✅ 재귀 호출과 함께 .join() 을 사용해서 결과를 통합합니다.
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class ForkJoinEx {
static final ForkJoinPool pool = new ForkJoinPool();
public static void main(String[] args) {
long from = 1L, to = 100_000_000L;
SumTask task = new SumTask(from, to);
long start = System.currentTimeMillis();
Long result = pool.invoke(task);
System.out.println("Elapsed time(4 core):" + (System.currentTimeMillis() - start));
System.out.printf("sum of %d~%d=%d%n", from, to, result);
System.out.println();
result = 0L;
start = System.currentTimeMillis();
for (long i = from; i <= to; i++) {
result += i;
}
System.out.println("Elapsed time(1 core):" + (System.currentTimeMillis() - start));
System.out.printf("sum of %d~%d=%d%n", from, to, result);
}
}
class SumTask extends RecursiveTask<Long> {
long from, to;
public SumTask(long from, long to) {
this.from = from;
this.to = to;
}
@Override
protected Long compute() {
long size = to - from + 1;
if (size <= 5) {
return sum();
}
long half = (from + to) / 2;
SumTask leftSum = new SumTask(from, half);
SumTask rightSum = new SumTask(half+1, to);
leftSum.fork();
return rightSum.compute() + leftSum.join();
}
long sum() {
long tmp = 0L;
for (long i = from; i <= to; i++) {
tmp += i;
}
return tmp;
}
}