-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
결론
synchronized 블록에서 사용되던 동시성 제어 메소드
- wait : 갖고 있던 락을 해제하고, 대기한다.
- notify : 임의의 스레드를 깨운다.
이거 말고 고수준 동시성 유틸리티를 사용하자!
고수준 유틸리티
java.util.concurrent의 고수준 유틸리티는 세 종류
- 실행자 프레임워크 [아이템 80] 스레드보다는 실행자, 태스크, 스트림을 애용하라 #81
- 동시성 컬렉션 (concurrent collection)
- 동기화 장치 (synchronizer)
동시성 컬렉션이란?
List, Queue, Map과 같은 표준 컬렉션 인터페이스에 동시성을 더한 컬렉션
- 각자 내부에서 동기화 수행
- 동시성을 무력화할 수 없음 = 외부에서 추가 락을 사용하면 속도만 ↓
각자 동기화 수행 -> 여러 기본 메소드를 하나의 원자적 동작으로 묶을 수 없음
- 이를 위해
상태 의존적 수정메소드 지원 - ex) Map의
putIfAbsent(key, value)- 주어진 키에 매핑된 값이 아직 없을 때만' 값을 넣는다.
- 기존 값이 있으면 기존 값을, 없으면 null 반환
ConcurrentHashMap
Collections.synchronizedMap보다 ConcurrentHashMap의 성능이 더욱 좋다.
- Hash를 사용한다.
- read는 non-blocking
- write는 특정 세그먼트나 버킷에 락을 걸어 효율적
작업 완료를 기다리는 컬렉션
BlockingQueue
take메소드는 큐의 첫번째 원소를 꺼낸다. 만약 큐가 비어있다면 새로운 원소가 추가될 때까지 기다린다.- Producer & Consumer 큐로 사용하기 좋다.
ThreadPoolExecutor를 포함한 대부분의 실행자 서비스가BlockingQueue를 사용
동기화 장치란?
스레드가 다른 스레드를 기다릴 수 있게 해서, 서로의 작업을 조율할 수 있게 한다.
- 자주 사용 :
CountDownLatch/Semaphore - 덜 사용 :
CyclicBarrier/Exchanger - 가장 강력 :
Phaser
CountDownLatch (latch; 걸쇠)
일회성 장벽으로, 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 대기하도록 한다.
- 생성자 파라미터의 int 값은 latch의
countDown메소드를 몇 번 호출해야 대기 중인 스레드들을 깨우는지 결정
public class CountDownLatch {
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
}CountDownLatch를 사용한 시간 체크
동일한 작업을 여러 실행자가 동시에 시작해 모두 완료하는 시작을 체크하는 메소드
- wait와 notify만으로 구현하려면 매우 어렵다.
- 파라미터: 동작을 실행할 실행자(Executor) / 동시에 몇 개의 작업을 수행할지 동시성 수준(int) / 수행할 동작(Runnable)
- 순서
- 타이머 스레드가 시계를 시작하기 전에, 모든 작업자 스레드는 동작을 수행할 준비를 마친다.
- 마지막 작업자 스레드가 준비를 마치면, 타이머 스레드가 시작 신호를 보낸다.
- 마지막 작업자 스레드가 동작을 마치자마자 타이머 스레드는 시계를 멈춘다.
public static long time(Executor executor, int concurrency,
Runnable action) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(concurrency);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(() -> {
// 타이머에게 준비가 됐음을 알린다.
ready.countDown();
try {
// 모든 작업자 스레드가 준비될 때까지 기다린다.
start.await();
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 타이머에게 작업을 마쳤음을 알린다.
done.countDown();
}
});
}
ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
long startNanos = System.nanoTime();
start.countDown(); // 작업자들을 깨운다.
done.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
return System.nanoTime() - startNanos;
}참고할 점
- Executor 객체는 concurrency로 지정한 동시성 수준만큼의 스레드를 생성할 수 있어야 한다.
그렇지 않으면 latch가 풀리지 않아, 스레드 기아 교착상태(thread starvation deadlock)에 빠진다. InterruptedExceptioncatch 절에서Thread.currentThread().interrupt()를 사용해야 실행자가 인터럽트를 적절히 처리한다.
작업자 스레드는 run 메소드에서 빠져나오면서 실행자에게 되살린 interrupt를 알릴 수 있기 때문- 시간 간격을 잴 땐
System.currentTimeMills가 아닌System.nanoTime을 사용하자.
더 정밀하며 시스템의 시간 보정에 영향받지 않기 때문 - 1초 이하의 정밀한 시간 측정은 jmh 같은 특수 프레임워크를 사용해야 한다.
레거시의 wait & notify를 만났을 때
wait 메소드는 스레드가 어떤 조건이 충족되기를 기다리게 한다.
- wait는 반드시 그 객체를 잠근 동기화 영역 안에서 호출해야 한다.
- 반드시 대기 반복문(wait loop) 관용구를 사용하여, 이미 조건이 충족되었을 경우 wait를 건너뛰게 하자. 반복문 밖에서 호출하지 말자.
- 조건을 검사하지 않는 경우 상황에 따라 대기 상태의 스레드를 깨울 수 없을지도 모른다.
synchronized (obj) {
while (조건이 충족되지 않았다) {
obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다. 누군가 notify를 해줘야 다음 동작 수행.
}
... // 조건이 충족됐을 때의 동작을 수행한다.
}또한 조건이 충족되지 않았을 때 다시 대기함으로써, 안전 실패를 막을 수 있다.
조건을 반복해서 검사하지 않으면, 조건이 만족되지 않아도 스레드가 깨어날 수 있는 상황이 몇 가지 있기 때문이다.
- 스레드가
notify를 호출해서 대기중인 스레드가 깨어나는 사이에, 다른 스레드가 락을 얻어 동작을 수행한다. - 조건이 만족되지 않았음에도 다른 스레드가 악의적으로
notify를 호출한다 (공개된 객체를 락으로 사용하는 경우) - 다른 스레드가 조건의 일부만 충족이 되었음에도,
notifyAll을 호출한다. - 대기 중인 스레드가 (드물게)
notify없이도 깨어나는 경우가 있다 (허위 각성; spurious wakeup; 1번처럼 시간 간극으로 인한 것)
synchronized (obj) {
obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다. 누군가 notify를 해줘야 다음 동작 수행.
... // 조건이 충족됐을 때의 동작을 수행한다.
}notify vs notifyAll
- 일반적으로 notifyAll (조건 확인 반복문이 걸려있어서 OK)
- 모든 스레드가 같은 조건을 기다리고, 조건이 한 번 충족될 때 단 하나의 스레드만 수행한다면 notify로 최적화
- 공개된 객체를 락으로 사용하는 경우 위험을 대비하기 위해 notify
참고
- String의
intern메소드는 String pool에서 리터럴 문자열이 이미 존재하는지 체크한다. 존재하면 해당 문자열을 반환하고, 아니면 리터럴을 String pool에 넣고 그 참조를 반환. 책에서는 ConcurrentHashMap으로 이 기능을 구현하고 있음.String a = "apple"; String b = new String("apple"); String c = b.intern() System.out.println(a==b); // false System.out.println(a==c); // true