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

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)