예전에는 wait와 notify가 의미가 있었지만, 이제 java.util.concurrent가 매우 좋음으로, 왠만하면 이걸 쓰자.
java.util.concurrent는 세 범주로 나뉘어질 수 있다.
실행자 프레임워크 : 아이템 #80 에서 살펴 보았다.
동시성 컬렉션
동기화 장치
동시성 컬렉션
동시성 컬렉션은 List, Queue, Map과 같은 표준 인터페이스의 동시성을 가미한 고성능 컬렉션이다. - 아이템 #79
동시성 컬렉션을 사용할때에는 외부에서 lock을 걸면 오히려 속도가 느려지니, 그냥 쓰도록 하자.
map
동시성을 무력화 하지 못함으로, '상태 의존적 수정' 메서드들이 추가 되었다. 예를 들면 Map의 putIfAbsent는 주어진 키값이 없을때만 value를 넣는다. 기존값이 있었다면 해당값을 반환하고, 없었다면, null을 반환한다.
String.intern을 다음과 같이 구현할 수 있다.
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;
}
- 기존 Collections.synchronizedMap 보다는 ConcurrentHashMap을 사용하는게 훨신 좋음으로, 새거 쓰자.
#### queue
- BlockingQueue에 추가된 메서드중 take는 큐의 첫 원소를 꺼내는데, 만약 큐가 비어 있으면, 큐가 추가될때까지 기다린다.
- 기존에 큐가 존재하는지 확인하고, 없으면 기다렸다가 루프를 도는등의 처리가 필요가 없다.
- ThreadPoolExecutor를 포함한 대부분의 실행자 서비스(아이템 #80) 구현체에서 BlockingQueue를 사용한다.
### 동기화 장치
- CountDownLatch : 아래는 action을 concurrency만큼을 동시에 실행했을때 걸리는 총 시간을 계산하는 method
```Java
public static long time(Executor executor, int concurrency, Runnable action) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(concurrency);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(done);
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;
}
위 코드에서, executor의 동시 실행 가능 개수가 concurrency보다 적다면, 교착상태에 빠지게 된다. 이를 막기 위해서 InterruptedException을 받아 interrupt처리를 해준다.
참고 : System.currentTimeMillis가 아닌 System.nanoTIme을 사용하자. 이게 더 정확하고 정밀함
wait, notify
wait, notify를 사용할때는 반드시 동기화 영역 안에서 사용해야 하며, 항상 반복문 안에서 사용하여야 한다.
synchronized (obj) {
while (<조건 이 충족되지 안았다>)
obj.wait();
// 뭔가 할일
}
만약에 대기조건이 만족하지 않았는데, 대기를 하게 되면, 영원히 깨어나지 않는 불상사가 생길 수 있음
while문 안에 있는 이유는, 조건이 만족하지 않았음에도 스레드가 깨어나는 경우가 있다.
notify를 호출 하였지만, 다른 스레드가 다시 락을 거는 경우
조건이 만족하지 않았지만, 실수 혹은 악의로 notify를 호출하는 경우
대기 중인 스레드중 일부만 조건이 충족되어도 notifyAll을 통해서 모두를 깨울 수 있음
드물게 notify없이 깨어나는 경우가 있음. 허위 각성(spurious wakeup)이란 현상
notify보다는 notifyAll을 통해서 모두를 깨우고, while 조건으로 확인하는것을 권장
예전에는 wait와 notify가 의미가 있었지만, 이제 java.util.concurrent가 매우 좋음으로, 왠만하면 이걸 쓰자.
java.util.concurrent는 세 범주로 나뉘어질 수 있다.
동시성 컬렉션
map
public static String intern(String s) { String previousValue = map.putIfAbsent(s, s); return previousValue == null ? s : previousValue; }
wait, notify