고수준 동시성 유틸리티가 나오기 이전에는 wait과 notify를 하드코딩 해서 사용하였지다
고수준의 동시성 유틸리티가 나오면서 wait과 notify를 사용해야할 이유가 많이 줄었다.
따라서 wait과 notify는 올바르게 사용하기 아주 까다로우니 고수준 동시성 유틸리티를 사용하자
java.util.concurrent의 고수준 유틸리느는 새 범주로 나뉨
1. 실행자 프레임 워크 (이전 장에서 확인)
2. 동시성 컬렉션 (이번에)
3. 동기화 장치 (이번에)
동시성 컬렉션
List, Queue, Map같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션
병행성을 높이기 위해 동기화를 내부에서 수행함.
동시성 컬렉션에서 동시성을 무력화 하는건 불가능
외부에서 락을 추가로 사용하면 오히려 속도가 느려짐
그래서 상태 의존성 수정 메소드 들이 추가 됨 (몇가지 기본 연산들을 하나의 원자적 연산으로 묶음)
예를 들면 아래와 같은 ConcurrentMap.putIfAbsent(K, V)
- 주어진 키에 매핑된 값이 아직 없을때 새값을 집어 넣음
- 기존 값이 있을땐 기존값 반환, 없으면 null
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;
}
ConcurrentMap으로 구현한 동시성 정규화 맵 (최적은 아님)
ConccruentHashMap은 get 같은 검색 기능에 최적화됨
get을 먼저 호출하여 필요할 때만 putIfAbsent를 호출하면 더 빠르다.
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;
}
ConcurrentHashMap은 동시성이 뛰어나며 속도도 무척 빠름
Collections.synchronizedMap보다 ConcurrentHashMap을 사용하는게 훨씬 좋다.
Queue를 확장한 BlockingQueue에 추가된 메서드 중 take는 큐의 첫 원소를 꺼냄
큐가 비었다면 새로운 원소가 추가될때까지 기다림
이런 특성 때문에 BlockingQueue는 작업큐(생산자-소비자 큐)로 쓰기에 적합
대부분 실행자 서비스 구현체에서 이 BlockingQueue를 사용한다.
동기화 장치
- 스레드가 다른 스레드를 기다릴 수 있게 하여, 서로 작업을 조율할 수 있게 해줌
- CountDownLatch와 Semaphore이고 가장 강력한 장치는 Phaser
CountDownLath
- 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 함.
- 유일한 생정자는 int값을 받으며 이 값이 래치의 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(() -> {
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;
}
어떤 동작들을 동시에 시작해 모두 완료하기까지의 시간을 재는 프로그램
1. 어떤 작업을 스레드풀에 등록
2. 모든 작업을 wait시킴
3. 시작 시간 세팅
4. 전체 작업 시작
5. 모든 스레드 작업이 끝날때 까지 대기
6. 진행된 시간을 계산하여 반환
wait과 notify만으로 구현하면 아주 난해한 코드가 됨
주의사항
- 실행자(executor)는 concurrency 매개변수로 지정한 동시성 수준만큼 스레드를 생성할 수 있어야 함
- 그렇지 못하면 이 메서드는 절 때 끝나지 않는다(기아 교착 상태)
- 시간을 잴 때는 항상 System.currentTimeMillis보다 System.nanoTime을 이용하자
- 더 정확하고 실시간 시계의 시간 보정에 영향을 받지 않음
- 락 객체의 wait메서드는 반드시 그 객체를 잠근 동기화 영역 안에서 호출
- wait을 사용할 때는 반드시 while을 사용하라 반복문 밖에서는 절대로 호출하지 말라
(wait이후 다시 락을 획득 했을 경우 실패 조건이 재검증 되지 않아 일관성이 깨질 수 있음)
- notifyAll을 사용하는게 합리적이고 안전하다. (모든 스레드가 깨어남을 항상 보장하여 정확한 결과를 얻음)
'책정리 > EfectiveJava' 카테고리의 다른 글
지연 초기화는 신중히 사용하라 (0) | 2019.04.22 |
---|---|
스레드 안전성 수준을 문서화 하라 (0) | 2019.04.22 |
스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2019.04.21 |
과도한 동기화는 피하라 (0) | 2019.04.17 |
공유 중인 가변 데이터는 동기화해 사용하라 (0) | 2019.04.03 |