wait와 notify를 꼭 사용해야 할 이유가 줄었다.
- 자바 5에서 도입된 고수준의 동시성 유틸리티가 wait, notify를 직접 사용해서 하던 일들을 대신 처리해줌(java.util.concurrent)
- 실행자 프레임워크 (Executors)
- 동시성 컬렉션(concurent collection)
- List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 부여한 것
- 각자 내부에서 동기화를 수행하므로 동시성을 무력화 하는 것이 불가능하고 외부에서 Lock를 사용하면 오히려 속도가 느려진다.
- 여러 메서드를 원자적으로 묶어서 호출하는 것 또한 불가능하다.
- 여러 기본 동작을 하나의 원자적 동작으로 묶는 상태 의존적 수정 메소드들이 추가되었다. (Java 8 default method 형태로 추가됨)
Map
default V putIfAbsent(K key,
V value){
//If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.
//Implementation Requirements:
//The default implementation is equivalent to, for this map:
V v = map.get(key); // get => null check => put 을 하나의 원자적 메소드로 묶음
if (v == null)
v = map.put(key, value);
return v;
}
String.intern 메서드는 String Constant Pool에 해당 문자열이 존재하면 반환하고 없다면 등록 후 반환하는 메서드이다.
ConcurrentMap putIfAbsent를 활용한버전의 String.intern(책에서는 6배 더 빠르다고 나옴)
Collections.synchronizedMap 보다는 ConcurrentHashMap를 사용하는 것이 좋다.
동기화된 맵보다 동시성 맵을 사용하는 것만으로 성능이 개선된다.
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; // 원래 없었다면 null
}
Queue
BlockingQueue(작업이 성공적으로 완료될떄까지 기다린다.(흐름을 차단한다.)
BlockingQueue.take(첫번째 원소를 꺼낸다. 단 큐가 비어있다면, 새로운 원소가 추가될떄까지 기라딘다. (생산자 -소비자 패턴에 적합하다. 소비자에서 큐가 비어있는걸 따로 처리해줄필요가 없기 때문에)
ThreadpoolExecutor등 대부분의 실행자 패턴에서 BlockingQueue를 사용해서 구현한다.
동기화 장치(synchronizer)
스레드끼리의 동기화를 조율해주는 장치
CountDownLatch
- 특정 동작(메소드 등)이 N번 수행되기 전까지 한 개 이상의 스레드를 붙잡아놓는다.
- await -> 생성자로 넘겨준 횟수만큼의 countDown이 호출되기전까지 스레드 실행을 막아놓음
void countdown() throws InterruptedException {
final int concurrency = 3;
ExecutorService executorService = Executors.newFixedThreadPool(3); // 스레드풀에 스레드가 동시성 수준 이상만큼 있어야한다.
CountDownLatch ready = new CountDownLatch(concurrency); // 동시성 수준
CountDownLatch start = new CountDownLatch(1); // 시작 방아쇠
CountDownLatch done = new CountDownLatch(concurrency);
for (int i = 0; i < concurrency; i++) {
executorService.execute(() -> {
ready.countDown(); // 준비
try {
start.await(); //26번쨰 라인에서 countDown 1번 호출될떄 다시 시작
System.out.println("작업을 수행한다");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown(); // 작업 끝나면 done Countdown --> 작업이 총 세개가 동시니까 수를 일치시켜
}
});
}
ready.await(); // 동시성 수준(3)만큼 countDown이 호출될떄까지(준비될 때까지) 기다림
long startNanos = System.nanoTime(); // 준비되고나서
start.countDown(); // 준비됐으니까 start CountDown줌
done.await();
System.out.println(System.nanoTime() - startNanos);
//System.nanoTime()가 System.currentTimemillis보다 정확하고 시스템의 실시간 시계 시간 보정에 영향 받지 않는다.
}
그럼에도 레거시 코드에서 wait, notify를 꼭 써야만 하는 경우.
일반적으로 notify보다는 notifyAll을 사용하는 것이 안전하며, wait는 항상 while문 내부에서 호출하도록 해야 한다.
- notify는 waitset에 있는 랜덤하게 스레드 하나를 꺠우고 notifyAll은 스레드를 전부다 깨우고 그 중하나만이 락을 획득한다.
(notify로 인해 전혀 관계 없는 쓰레드가 깨어났는데 별 동작없이 그냥 가버린다면 기존 스레드들은 영원히 잠잘것이다)
synchronized (obj){
while (조건이 충족되지 않았다){
obj.wait()
}
}
'JAVA > Effective Java' 카테고리의 다른 글
item 87) 커스텀 직렬화 형태를 고려해보라 (0) | 2021.04.25 |
---|---|
item 88) readObject는 방어적으로 작성하라 (0) | 2021.04.24 |
item 73) 추상화 수준에 맞는 예외를 던지라 (0) | 2021.04.10 |
item74) 메서드가 던지는 모든 예외를 문서화해라 (0) | 2021.04.10 |
item 72) 표준 예외를 사용하라. (0) | 2021.04.04 |