69. wait나 notify 대신 병렬성 유틸리티를 이용하라.
과거처럼 wait나 notify를 직접 구현하지 말고(wait와 nofiy를 정확하게 사용하기 어렵기 때문에), 자바 플랫폼(1.5이상)이 제공하는 고수준 병행성 유틸리티(high-level concurrency utility)를 이용하라.
병행성 유틸리티 분류
- 실행자 프레임워크(Executor Framework) : 규칙 68 참고
- 병행 컬렉션(concurrent collection)
- 동기자(synchronizer)
병행 컬렉션(concurrent collection)
표준 Collection 인터페이스(ex:List, Queue, Map)에 고성능 병행 컬렉션 구현을 제공하며, 병행성을 높이기 위해 동기화를 내부적으로 처리
컬렉션 외부에서 병행성을 처리하는 것이 불가능. 락을 걸어봐야 락 중복으로 인해 성능만 나빠짐(규칙 67 참고)
위 문제를 해결하기 위해서 상태 종속 변경 연산을 제공(몇가지 기본 연산들을 하나의 원자적 연산으로 묶은 것)
ex> ConcurrentMap.putIfAbsent(K, V)
: 키에 해당하는 키에 해당하는 값이 없을 때문 주어진 값을 집어 넣고,
해당 키에 대응하여 저장되어 있었던 기존 값을 반환. 값이 없으면 null 반환
// ConcurrentMap으로 구현한 병행 정규화 맵
private static final ConcurrentMap<String, Stirng> map =
new ConcurrentHashMap<String, String>();
// 최적이 아님
public static String intern(String s) {
String previousValue = map.putIfAbsent(s, s);
return previousValue = null ? s : previousValue;
}
// 최적화 버전
public static String intern(String s) {
String result = map.get(s);
if (result == null) {
result = map.putInAbsent(s, s);
if (result == null)
result = s;
}
return result;
}
병행 처리에 추천하는 병행 컬렉션
ConcurrentHashMap
:Collections.synchronizedMap
이나Hashtable
대신,ConcurrentHashMap
을 사용하자.CopyOnWriteArrayList
:ArrayList
의 변종으로 내부 배열을 통째로 복사하는 방식으로 쓰기 연산을 지원한다.( 내부 배열을 절대로 수정하지 않음)BlockingQueue
: 봉쇄 연산(blocking operation : task) 가능
동기자(synchronizer)
스레드들이 서로 기다릴 수 있도록 하여, 상호 혐력이 가능하게 함
카운트 다운 래치(CountDownLatch)
- 일회성 배리어(barrier)로서 하나 이상의 스레드가 작업을 마칠 때까지 다른 여러 스레드가 대기할 수 있도록 함
- 대기 중인 스레드가 진행할 수 있으려면 CountDownLatch생성자에 정의한 횟수만큼 countdown 메서드가 호출되어야 한다.
// 작업의 병령 수행 시간을 재는 간단한 프레임워크
public static long time(Executor executor, int concurrency,
final Runnable action) throws InterruptedException {
// 작업 스레드가 타이머 스레드에게 실행 준비가 끝났음을 알리려고 사
final CountDownLatch ready = new CountDownLatch(concurrency);
// 작업 시작(타이머)
final CountDownLatch start = new CountDownLatch(1);
// 작업 완료
final CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executor.execute(new Runnable() {
public void run() {
ready.countDown(); // 타이머에게 준비됨을 알림
try {
start.await(); // 다른 작업스레드가 준비될 때까지 대기
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown(); // 타이머에게 끝났음을 알림
}
}
})
}
ready.await(); // 모든 작업 스레드가 준비될 때까지 대기
// 특정 구간의 실행시간을 잴 때는 System.currentTimeMillis대신 System.nanoTime을 사용하자.
// 그래야 더 정밀하게 시간을 잴 수 있을 뿐더러, 시스템의 실시간 클락(real-time clock)변동에도 영향을 받지 않는다.
long startNanos = System.nanoTime();
start.countDown(); // 출발
done.await(); // 모든 작업 스레드가 끝날 때까지 대기
return System.nanoTime() - startNanos;
}
작업(Runnable 인자)을 병렬로 실행할 횟수(concurrency) 만큼 돌려서 실행되는 시간을 구하는 메서드
- 작업을 쓰레드에 등록을 해서 (
final Runnable action
을executor.execute
에 등록) - 전체 작업 쓰레드가 준비를 대기한 다음 (
ready.await()
) - 시작시간을 재고 (
long startNanos = System.nanoTime()
) - 전체 작업이 시작되고 (
start.countDown()
) - 전체 작업이 마감이 된 후 (
done.await()
) - 진행된 시간을 계산해서 반환 (
return System.nanoTime() - startNanos
)
[주의사항]
- 실행자(Executor인자)가 주어진 병행수(concurrency) 보다 같거나 많은 수의 스레드를 동시에 생성할 수 있어야함
- 시간을 잴 때는 System.currentTimeMillis 대신 System.nanoTime를 사용함. 더 정밀하고, 시스템의 실시간 클락의 변동에도 영향을 받지 않음
wait, notify
wait메서드를 호출할 때는 반드시 아래의 대기 순환문(wait loop)숙어대로 하자.
synchronized( obj ){
while( 실패 조건 )
obj.wait(); // (락 해제, 깨어나면 다시 락 획득)
... // 조건이 만족되면 그에 맞는 작업 실행
}
while 문으로 실패조건을 확인하는 이유?
- if로 체크할 경우 wait 이후에 다시 락을 획득 했을 시 실패 조건 재검증이 안되서 일관성이 깨질 수 있음
- 락을 다시 획득하는(스레드를 다시 깨울) 경우 가능하면 notify 대신 notifyAll을 사용하라
- JVM이 알아서 락을 획득할 스레드의 우선순위를 적절하게 정해줘서 영원히 잠드는(대기상태인) 스레드가 없을 것이다.
- 악의적으로 또는 객체의 API 노출 등의 실수로 notify가 호출되는 경우 무한 대기가 걸릴 수 있음.
결론
자바API가 신규로 제공하는 고수준의 병행성 유틸리티를 사용하라. notify
, wait
를 사용할 이유가 없다.
참고 : 자바 병렬 프로그래밍 : 멀티코어를 100% 활용하는(Java Concurrency in Practice)