Item 69. wait나 notify 대신 병렬성 유틸리티를 이용하라.

과거처럼 wait나 notify를 직접 구현하지 말고(wait와 nofiy를 정확하게 사용하기 어렵기 때문에), 자바 플랫폼(1.5이상)이 제공하는 고수준 병행성 유틸리티(high-level concurrency utility)를 이용하라.

병행성 유틸리티 분류

병행 컬렉션(concurrent collection)

표준 Collection 인터페이스(ex:List, Queue, Map)에 고성능 병행 컬렉션 구현을 제공하며, 병행성을 높이기 위해 동기화를 내부적으로 처리

컬렉션 외부에서 병행성을 처리하는 것이 불가능. 락을 걸어봐야 락 중복으로 인해 성능만 나빠짐(규칙 67 참고)

위 문제를 해결하기 위해서 상태 종속 변경 연산을 제공(몇가지 기본 연산들을 하나의 원자적 연산으로 묶은 것)

ex> ConcurrentMap.putIfAbsent(K, V) : 키에 해당하는 키에 해당하는 값이 없을 때문 주어진 값을 집어 넣고, 해당 키에 대응하여 저장되어 있었던 기존 값을 반환. 값이 없으면 null 반환


// ConcurrentMap으로 구현한 병행 정규화 맵
private static final ConcurrentMap<String, Stirng> map =
        new ConcurrentHashMap<String, String>();

// 최적이 아님
public static String intern(String s) {
    String previousValue = map.putIfAbsent(s, s);
    return previousValue = null ? s : previousValue;
}

// 최적화 버전
public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.putInAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}
병행 처리에 추천하는 병행 컬렉션

동기자(synchronizer)

스레드들이 서로 기다릴 수 있도록 하여, 상호 혐력이 가능하게 함

카운트 다운 래치(CountDownLatch)
// 작업의 병령 수행 시간을 재는 간단한 프레임워크
public static long time(Executor executor, int concurrency,
        final Runnable action) throws InterruptedException {
    // 작업 스레드가 타이머 스레드에게 실행 준비가 끝났음을 알리려고 사
    final CountDownLatch ready = new CountDownLatch(concurrency);
    // 작업 시작(타이머)
    final CountDownLatch start = new CountDownLatch(1);
    // 작업 완료
    final CountDownLatch done = new CountDownLatch(concurrency);
    for (int i = 0; i < concurrency; i++) {
        executor.execute(new Runnable() {
            public void run() {
                ready.countDown();  // 타이머에게 준비됨을 알림
                try {
                    start.await();  // 다른 작업스레드가 준비될 때까지 대기
                    action.run();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();   // 타이머에게 끝났음을 알림
                }
            }
        })
    }
    ready.await();      // 모든 작업 스레드가 준비될 때까지 대기
    // 특정 구간의 실행시간을 잴 때는 System.currentTimeMillis대신 System.nanoTime을 사용하자. 
    // 그래야 더 정밀하게 시간을 잴 수 있을 뿐더러, 시스템의 실시간 클락(real-time clock)변동에도 영향을 받지 않는다.
    long startNanos = System.nanoTime();
    start.countDown();  // 출발
    done.await();       // 모든 작업 스레드가 끝날 때까지 대기
    return System.nanoTime() - startNanos;
}

작업(Runnable 인자)을 병렬로 실행할 횟수(concurrency) 만큼 돌려서 실행되는 시간을 구하는 함수

  1. 작업을 쓰레드에 등록을 해서 ( final Runnable actionexecutor.execute에 등록)
  2. 전체 작업 쓰레드가 준비를 대기한 다음 (ready.await())
  3. 시작시간을 재고 (long startNanos = System.nanoTime())
  4. 전체 작업이 시작되고 (start.countDown())
  5. 전체 작업이 마감이 된 후 (done.await())
  6. 진행된 시간을 계산해서 반환 (return System.nanoTime() - startNanos)

[주의사항]

wait, notify

wait함수를 호출할 때는 반드시 아래의 대기 순환문(wait loop)숙어대로 하자.

synchronized( obj ){
    while( 실패 조건 )
        obj.wait(); // (락 해제, 깨어나면 다시 락 획득)
    ... // 조건이 만족되면 그에 맞는 작업 실행
}

while 문으로 실패조건을 확인하는 이유?

결론

자바API가 신규로 제공하는 고수준의 병행성 유틸리티를 사용하라. notify, wait를 사용할 이유가 없다.

참고 : 자바 병렬 프로그래밍 : 멀티코어를 100% 활용하는(Java Concurrency in Practice)