Item 81 : wait와 notify보다는 동시성 유틸리티를 애용하라
2021. 9. 19. 10:43
- 과거에 비해 wait와 notify를 사용해야할 이유가 많이 줄었다.
- 나 또한 코딩을 해보며 써본 기억이 없는 것 같다.
- wiat의 기능은 스레드를 기다리게 만들고 notify는 깨우는 기능이다.
- wait와 notify는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
- java.utility.concurrnet의 고수준 유틸리는 세 범주로 나눌 수 있다.
- 실행자 프레임워크(Executor)
- 동시성 컬렉션(concurrent collection)
- 동기화 장치(synchronizer)
- 실행자 프레임워크는 아이템 80에서 다뤄 보았다.
동시성 컬렉션
- 동시성 컬렉션은 List, Queue, Map같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다.
- 동시성을 위해 내부에서 동기화를 수행한다.
- 동시성 컬렉션에서 동시성을 무력화하는건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다.
- 동시성 컬렉션에서 동시성을 무력화하지 못하므로 여러 메소드를 원자적으로 묶어 호출하는 일 역시 불가능하다.
- 여러 기본 동작을 하나의 원자적 동작으로 묶는
상태 의존적 수정
메소드들이 추가되었다.- 예를 들어 Map의 putIfAbsent(key, value) 메소드는 주어진 키에 매핑된 값이 아직 없을때만 새 값을 집어넣는다.
- 그리고 기존 값이 있었다면 그 값을 반환하고 없었다면 null을 반환한다.
- 여러 기본 동작을 하나의 원자적 동작으로 묶는
/** String.intern의 동작을 흉내내어 구현한 메소드
* String.intern은 문자열이 존재하면 반환, 아닌 경우에 문자열 풀에 등록하는 메소드이다.
* ConcurrentMap으로 구현한 동시성 정규화 맵 - 최적화는 아님
*/
private static final ConcurrentMap<String, String> map =
new ConcurrentHashMap<>();
public static String intern(String s) {
String previousValue = map.putIfAbsent(s, s);
return previousValue == null ? s : previousValue;
}
- ConcurrentHashMap은 get같은 검색 기능에 최적화 되었다.
- 따라서 get을 먼저 호출하여 필요할 때만 puIfAbsent를 호출하면 더 빠르다.
// ConcurrentMap으로 구현한 동시성 정규화 맵 - 더 빠르다
public static String intern(String s) {
String result = map.get(s);
if (result == null) {
result = map.putIfAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
동시성 컬렉션은 동기화한 컬렉션을 낡은 유산으로 만들었다.
- Collections.synchronizedMap 보다는 ConcurrentHashMap을 사용하는게 훨씬 좋다.
컬렉션 인터페이스 중 일부는 작업이 성공적으로 완료될 때까지 기다리도록 확장되었다.
- Queue를 확장한 BlockingQueue에 추가된 메소드 중 take는 큐의 첫 원소를 꺼낸다.
- 이때 만약 큐가 비었다면 새로운 원소가 추가될 때까지 기다린다.
- 이런 특성 덕분에 생산자-소비자 큐로 쓰기에 적합하다.
- ThreadPoolExecutor를 포함한 대부분의 실행자 서비스 구현체에서 이 BlokingQueue를 사용한다.
동기화 장치는 스레드가 다른 스레드를 기다릴 수 있게 하여 서로 작업을 조율할 수 있게 해준다.
- 가장 자주 쓰이는 동기화 장치
- CountDownLatch
- Semaphore
- 상대적으로 덜 쓰이는 장치
- CyclicBarrier
- Exchanger
- 가장 강력한 동기화 장치
- Phaser
- CountDownLatch는 일회성 장벽으로 (Latch : 걸쇠), 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 한다.
- CountDownLatch의 유일한 생성자는 int 값을 받으며, 이 값이 래치의 countDown 메소드를 몇번 호출해야 대기 중인 스레드들을 꺠우는지를 결정한다.
/**
* CountDownLatch를 이용한 동시 실행 시간을 재는 간단한 프레임워크
*/
public static long time(Executor executor, int concurrency, Runnable action)
throws InterruptedException {
CountDownLatch ready = new CountDownLatch(concurrency); //동시에 몇개 수행할 지 결정
CountDownLatch start = new CountDownLatch();
CountDownLatch done = new ContDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(() -> {
// 타이머에게 준비를 마쳤음을 알림
ready.countDown();
try {
// 모든 작업자 스레드가 준비될 때까지 대기
start.await();
action.run();
} catch (InterruptedException e) {
Thread.concurrentThread().interrupt();
} finally {
// 타이머에게 작업을 마쳤음을 알림
done.countDown();
}
});
}
ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
long startNanos = System.nanoTime();
start.countDown(); // 작업자들을 깨운다.
down.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
return System.nanoTime() - startNanos;
}
- ready 래치는 작업자 스레드들이 준비가 완료됐음을 타이머 스레드에 통지할 때 사용
- 통지를 끝낸 작업자 스레드들은 두번째 래치인 start가 열리길 기다림
- 마지막 작업자 스레드가 ready.countDown을 호출하면 타이머 스레드가 시작 시각을 기롤하고 start.countDown을 호출하여 기다리던 작업자 스레드들을 깨움
- 그 직후 타이머 스레드는 세 번째 래치인 done이 열리기를 기다린다.
- done 래치는 마지막 남은 작업자 스레드가 동작을 마치고 done.countDown을 호출하면 열린다.
- 타이머 스레드는 done 래치가 열리자마자 꺠어나 종료시작을 기록
새로운 코드라면 언제나 wait와 notify가 아닌 동시성 유틸리티를 써야한다.
- 하지만 어쩔 수없이 레거시 코드를 다뤄야 할때도 있을 것이다.
- wait 메소드는 스레드가 어떤 조건이 충족되기를 기다리게 할 때 사용한다.
- 락 객체의 wait 메소드는 반드시 그 객체를 잠근 동기화 영역 안에서 호출해야한다
// wait 메소드를 사용하는 표준 방식
synchronized (obj) {
while (<조건이 충족되지 않음>) {
obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다.
}
... // 조건이 충족됐을 때의 동작을 수행
}
- wait 메소드를 사용할 때는 반드시 대기 반복문(wait loop) 관용구를 사용하라.
- 반복문 밖에서는 절대로 호출하지 말자
조건이 만족되지 않았는데 스레드가 깨어날 수 있는 상황 몇가지
- 스레드가 notify를 호출한 다음 대기 중이던 스레드가 꺠어나는 사이에 다른 스레드가 락을 얻어 그 락이 보호하는 상태를 변경
- 조건이 만족되지 않았음에도 다른 스레드가 실수로 혹은 악의적으로 notify를 호출
- 깨우는 스레드는 지나치게 관대해서 대기중인 스레드 중 일부만 조건이 충족되어도 notifyAll을 호출해 모든 스레드를 깨울 수도 있음
- 대기 중인 스레드가 notif없이도 깨어나는 경우가 있다.
- 허위 각성 현상이다(spurious wakeup)
'책 > 이펙티브자바' 카테고리의 다른 글
Item 83 : 지연 초기화는 신중히 사용하라 (0) | 2021.09.20 |
---|---|
Item 82 : 스레드 안전성 수준을 문서화하라 (0) | 2021.09.19 |
Item 80 : 스레드보다는 실행자, 태스크, 스트림을 애용하라. (0) | 2021.09.17 |
Item 79 : 과도한 동기화는 피하라 (0) | 2021.09.16 |
Item 78 : 공유 중인 가변 데이터는 동기화해 사용하라 (0) | 2021.09.15 |