#69 wait와 notify 대신 동시성 유틸리티를 사용하자
자바에서는 동시성 유틸리티(wait와 notify를 이용해 직접 구현했던 작업들)를 제공하기 때문에, 굳이 어렵게 wait와 notify를 이용할 필요가 없습니다.
동시성 유틸리티를 사용하는 방법에 대해 알아보겠습니다.
List, Map, Queue와 같은 표준 컬렉션 인터페이스를 고성능의 동시적 구현체(컬렉션)로 제공합니다.
말 그대로 고성능의 동시성을 위해 내부적으로 나름의 동기화를 하고 있습니다.
따라서 별도로 lock을 걸어도 효과도 없고 느려지기만 합니다.
#장점 01 - 빠른 처리 속도
예를 들어, 동기화되는 컬렉션인 Hashtable이나 Collections.synchronizedMap을 이용하기보다는, ConcurrentHashMap 동시적 컬렉션을 이용하는 게 성능면에서 훨씬 좋습니다.
특별한 이유가 없는 한, 동시적 컬렉션을 이용합시다.
#장점 02 - 상태 종속 변경 연산(state-dependent modify operation) 지원
상태 종속 변경 연산이라는 이상한 말이 있는데, 그냥 여러 개의 기본 연산을 결합하여 단일 원자 연산으로 만든 것을 말합니다.
예를 들어, ConcurrentMap 인터페이스는 Map 인터페이스를 extends 하고 몇 개의 메서드를 추가했습니다.
그중 하나인 putIfAbsent(key, value)를 보면 containsKey와 put 메서드를 결합해서 만들었습니다.
이를 직접 구현하지 않고 상태 종속 변경 연산들을 이용하는 게 좋습니다.
// ConcurrentMap - putIfAbsent(key, value)
if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
#장점 03 - 블록 연산(blocking operation) 지원
블록 연산은 성공적으로 수행될 수 있을 때까지 대기 또는 블록 처리되는 연산을 말합니다.
예를 들어, BlockingQueue 인터페이스는 Queue 인터페이스를 extneds 하고 몇 개의 메서드를 추가했습니다.
생산자 스레드가 작업 항목을 큐에 넣고, 소비자 스레드가 그 큐로부터 항목을 받아 처리할 때 쓸 수 있기 때문에 이를 생산자-소비자 큐라고 합니다.
ExecutorService에서도 스레드 풀을 이 BlockingQueue를 이용하고 있습니다.
스레드가 다른 스레드를 대기시킬 수 있게 해주는 객체입니다.
동기자 중 하나인 CountDownLatch 클래스를 보면, 하나 이상의 스레드가 하나 이상의 다른 스레드를 대기시킬 수 있습니다.
아래와 같이 CountDownLatch 생성자를 통해 인자 값(count)을 받아, await 메서드를 통해 대기 상태를 만듭니다.
그리고 countDown 메서드를 통해 count 값이 0이 되면, 대기 상태를 해제합니다.
동시성 유틸리티가 아닌 wait와 notify를 직접 이용해야 하는 경우도 있습니다.
wait를 이용할 때는 동기화된 영역 내부에서 표준 이디엄 방식을 통해 구현되어야 합니다.
대기 전과 후의 조건을 검사하기 위해서입니다.
// wait 메서드의 사용을 위한 표준 이디엄
synchronized(obj) {
while(/* 대기 상태를 벗어날 조건을 만족하지 않으면 */)
obj.wait(); // obj 객체의 락을 해제하고, 깨워주기 기다린다.
... // 조건을 만족하면 적합한 동작을 수행합니다.
}
notify의 경우에는, 만약 스레드가 대기 전 상태라면 notify를 통해 wait 상태에서 깨어나지 않을 수 있습니다. (활동성이 보장되도록 주의)
따라서 notify 보다는 모든 스레드를 깨우는 notifyAll을 이용해서 대기 중인 모든 스레드를 깨우는 게 더 안전합니다.