Effective Java ‐ Item 81⚠️ - dnwls16071/Backend_Summary GitHub Wiki
아이템 81 - wait와 notify보다는 동시성 유틸리티를 애용하라.
wait와 notify는 올바르게 사용하기가 아주 까다로우니 고수준의 동시성 유틸리티를 사용하라.
wait과 notify는 쓰레드 상태 제어를 위한 메서드이다.
wait : 가지고 있던 고유 락을 해제하고 쓰레드를 잠들게 하는 역할
notify : 잠들어 있던 쓰레드 중 임의의 하나를 골라 꺠우는 역할
public class ThreadB extends Thread{
// 해당 쓰레드가 실행되면 자기 자신의 모니터링 락을 획득
// 5번 반복하면서 0.5초씩 쉬면서 total에 값을 누적
// 그후에 notify()메소드를 호출하여 wait하고 있는 쓰레드를 깨움
@Override
public void run(){
synchronized(this){
for(int i=0; i<5 ; i++){
System.out.println(i + "를 더합니다.");
total += i;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
notify(); // wait하고 있는 쓰레드를 깨움
}
}
}
public class ThreadA {
public static void main(String[] args){
// 앞에서 만든 쓰레드 B를 만든 후 start
// 해당 쓰레드가 실행되면, 해당 쓰레드는 run메소드 안에서 자신의 모니터링 락을 획득
ThreadB b = new ThreadB();
b.start();
// b에 대하여 동기화 블럭을 설정
// 만약 main 쓰레드가 아래의 블록을 위의 Thread보다 먼저 실행되었다면 wait를 하게 되면서 모니터링 락을 놓고 대기
synchronized(b){
try{
// b.wait()메소드를 호출.
// 메인쓰레드는 정지
// ThreadB가 5번 값을 더한 후 notify를 호출하게 되면 wait에서 깨어남
System.out.println("b가 완료될때까지 기다립니다.");
b.wait();
} catch(InterruptedException e){
e.printStackTrace();
}
// 깨어난 후 결과를 출력
System.out.println("Total is: " + b.total);
}
}
}
자바에서 지원되는 고수준 동시성 유틸리티
- Executor Framework
- 동시성 컬렉션(Concurrent Collection)
- 동기화(synchronized)
- 일반적인 컬렉션 + 동시성 = 동시성 컬렉션(CopyOnWriteArrayList, ConcurrentHashMap, ConcurrentLinkedQueue)
- 동시성 컬렉션의 동시성을 무력화하는 것은 불가능하며, 외부에서 락을 걸면 오히려 속도가 느려진다. 동시성을 무력화하지 못하므로 여러 메서드를 원자적으로 묶어 호출할 수 있다.
동시성 컬렉션 - 상태 의존적 메서드
putIfAbsent("key"); // 키에 매핑된 값이 없을 때에만 새 값을 집어넣고, 없으면 그 값을 반환한다.
Collections.synchronized~
- Collections에서 제공해주는 synchronized~()를 사용하여 동기화한 컬렉션을 만드는 것 보다는 동시성 컬렉션을 사용하는 것이 성능적으로도 훨씬 좋다.
동기화 장치
public interface BlockingQueue<E> extends Queue<E> {
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element becomes available.
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
// BlockingQueue의 take()는 큐의 원소를 꺼내는 역할을 하는데, 만약 큐가 비어있다면 새로운 원소가 추가될 때까지 기다린다.
// ThreadPoolExcutor을 포함한 대부분의 실행자 서비스(아이템 80)에서 BlockingQueue를 사용한다.
E take() throws InterruptedException;
}
동기화 장치의 종류 - CountDownLatch
- 하나 이상의 쓰레드가 또 다른 하나 이상의 쓰레드 작업이 끝날 때까지 기다리게 한다. 생성할 때 int 값을 받게 되는데, 이 값이
countDown()을 몇 번 호출해야 대기 중인 쓰레드를 깨우는지 결정한다.
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
try {
long result = time(executorService, 3, () -> System.out.println("hello"));
System.out.println("총 걸린 시간 : " + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
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(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;
}
}
- executor는 concurrency 매개변수로 지정한 값(위 코드에서는 3)만큼의 스레드를 생성할 수 있어야 한다. 그렇지 않으면 메서드 수행이 끝나지 않는데 이를 스레드 기아 교착 상태라고 한다.
wait()와 notify()를 사용해야하는 상황에서는?
- 반드시 동기화 영역 안에서만 사용해야 하며, 항상 반복문 안에서 사용해야 한다.
결론
wait()와 notify()를 쓸 이유가 거의(어쩌면 전혀) 없다.
- 만약 사용해야한다면 wait는 while문 안에서 호출해야한다.
notify()보다는 notifyAll()을 사용하자.
notify()를 사용하다면 응답 불가 상태에 빠지지 않도록 조심하자.