• 과거에 비해 wait와 notify를 사용해야할 이유가 많이 줄었다.
    • 나 또한 코딩을 해보며 써본 기억이 없는 것 같다.
  • wiat의 기능은 스레드를 기다리게 만들고 notify는 깨우는 기능이다.
  • wait와 notify는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
  • java.utility.concurrnet의 고수준 유틸리는 세 범주로 나눌 수 있다.
    • 실행자 프레임워크(Executor)
    • 동시성 컬렉션(concurrent collection)
    • 동기화 장치(synchronizer)
  • 실행자 프레임워크는 아이템 80에서 다뤄 보았다.

 

 

동시성 컬렉션

  • 동시성 컬렉션은 List, Queue, Map같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다.
  • 동시성을 위해 내부에서 동기화를 수행한다.
  • 동시성 컬렉션에서 동시성을 무력화하는건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다.
  • 동시성 컬렉션에서 동시성을 무력화하지 못하므로 여러 메소드를 원자적으로 묶어 호출하는 일 역시 불가능하다.
    • 여러 기본 동작을 하나의 원자적 동작으로 묶는 상태 의존적 수정 메소드들이 추가되었다.
      • 예를 들어 Map의 putIfAbsent(key, value) 메소드는 주어진 키에 매핑된 값이 아직 없을때만 새 값을 집어넣는다.
      • 그리고 기존 값이 있었다면 그 값을 반환하고 없었다면 null을 반환한다.
/** String.intern의 동작을 흉내내어 구현한 메소드
* String.intern은 문자열이 존재하면 반환, 아닌 경우에 문자열 풀에 등록하는 메소드이다.
* ConcurrentMap으로 구현한 동시성 정규화 맵 - 최적화는 아님
*/

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;
}
  • ConcurrentHashMap은 get같은 검색 기능에 최적화 되었다.
  • 따라서 get을 먼저 호출하여 필요할 때만 puIfAbsent를 호출하면 더 빠르다.
// ConcurrentMap으로 구현한 동시성 정규화 맵 - 더 빠르다

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;
}

 

 

동시성 컬렉션은 동기화한 컬렉션을 낡은 유산으로 만들었다.

  • Collections.synchronizedMap 보다는 ConcurrentHashMap을 사용하는게 훨씬 좋다.

 

 


 

컬렉션 인터페이스 중 일부는 작업이 성공적으로 완료될 때까지 기다리도록 확장되었다.

  • Queue를 확장한 BlockingQueue에 추가된 메소드 중 take는 큐의 첫 원소를 꺼낸다.
    • 이때 만약 큐가 비었다면 새로운 원소가 추가될 때까지 기다린다.
    • 이런 특성 덕분에 생산자-소비자 큐로 쓰기에 적합하다.
    • ThreadPoolExecutor를 포함한 대부분의 실행자 서비스 구현체에서 이 BlokingQueue를 사용한다.

 

 

동기화 장치는 스레드가 다른 스레드를 기다릴 수 있게 하여 서로 작업을 조율할 수 있게 해준다.

  • 가장 자주 쓰이는 동기화 장치
    • CountDownLatch
    • Semaphore
    • 상대적으로 덜 쓰이는 장치
      • CyclicBarrier
      • Exchanger
    • 가장 강력한 동기화 장치
      • Phaser
  • CountDownLatch는 일회성 장벽으로 (Latch : 걸쇠), 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때까지 기다리게 한다.
    • CountDownLatch의 유일한 생성자는 int 값을 받으며, 이 값이 래치의 countDown 메소드를 몇번 호출해야 대기 중인 스레드들을 꺠우는지를 결정한다.
/** 
* CountDownLatch를 이용한 동시 실행 시간을 재는 간단한 프레임워크
*/

public static long time(Executor executor, int concurrency, Runnable action)
                                throws InterruptedException {

    CountDownLatch ready = new CountDownLatch(concurrency); //동시에 몇개 수행할 지 결정
    CountDownLatch start = new CountDownLatch();
    CountDownLatch done = new ContDownLatch(concurrency);

    for (int i = 0; i < concurrency; i++) {
        executor.execute(() -> {
           // 타이머에게 준비를 마쳤음을 알림
            ready.countDown();
            try {
                // 모든 작업자 스레드가 준비될 때까지 대기
                start.await();
                action.run();
            } catch (InterruptedException e) {
                Thread.concurrentThread().interrupt();
            } finally {
                // 타이머에게 작업을 마쳤음을 알림
                done.countDown();
            }
        });
    }

    ready.await(); // 모든 작업자가 준비될 때까지 기다린다.
    long startNanos = System.nanoTime();
    start.countDown(); // 작업자들을 깨운다.
    down.await(); // 모든 작업자가 일을 끝마치기를 기다린다.
    return System.nanoTime() - startNanos;
}
  • ready 래치는 작업자 스레드들이 준비가 완료됐음을 타이머 스레드에 통지할 때 사용
    • 통지를 끝낸 작업자 스레드들은 두번째 래치인 start가 열리길 기다림
    • 마지막 작업자 스레드가 ready.countDown을 호출하면 타이머 스레드가 시작 시각을 기롤하고 start.countDown을 호출하여 기다리던 작업자 스레드들을 깨움
    • 그 직후 타이머 스레드는 세 번째 래치인 done이 열리기를 기다린다.
    • done 래치는 마지막 남은 작업자 스레드가 동작을 마치고 done.countDown을 호출하면 열린다.
    • 타이머 스레드는 done 래치가 열리자마자 꺠어나 종료시작을 기록

 

 

새로운 코드라면 언제나 wait와 notify가 아닌 동시성 유틸리티를 써야한다.

  • 하지만 어쩔 수없이 레거시 코드를 다뤄야 할때도 있을 것이다.
  • wait 메소드는 스레드가 어떤 조건이 충족되기를 기다리게 할 때 사용한다.
  • 락 객체의 wait 메소드는 반드시 그 객체를 잠근 동기화 영역 안에서 호출해야한다
// wait 메소드를 사용하는 표준 방식

synchronized (obj) {
    while (<조건이 충족되지 않음>) {
        obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다.
    }

    ... // 조건이 충족됐을 때의 동작을 수행
}
  • wait 메소드를 사용할 때는 반드시 대기 반복문(wait loop) 관용구를 사용하라.
  • 반복문 밖에서는 절대로 호출하지 말자

 

 

조건이 만족되지 않았는데 스레드가 깨어날 수 있는 상황 몇가지

  • 스레드가 notify를 호출한 다음 대기 중이던 스레드가 꺠어나는 사이에 다른 스레드가 락을 얻어 그 락이 보호하는 상태를 변경
  • 조건이 만족되지 않았음에도 다른 스레드가 실수로 혹은 악의적으로 notify를 호출
  • 깨우는 스레드는 지나치게 관대해서 대기중인 스레드 중 일부만 조건이 충족되어도 notifyAll을 호출해 모든 스레드를 깨울 수도 있음
  • 대기 중인 스레드가 notif없이도 깨어나는 경우가 있다.
    • 허위 각성 현상이다(spurious wakeup)

+ Recent posts