Effective Java ‐ Item 81⚠️ - dnwls16071/Backend_Summary GitHub Wiki

아이템 81 - wait와 notify보다는 동시성 유틸리티를 애용하라.

wait와 notify는 올바르게 사용하기가 아주 까다로우니 고수준의 동시성 유틸리티를 사용하라.

  • waitnotify는 쓰레드 상태 제어를 위한 메서드이다.
    • 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()를 사용하다면 응답 불가 상태에 빠지지 않도록 조심하자.