본문 바로가기

JAVA/Effective Java

item 81) wait와 notify보다는 동시성 유틸리티를 애용하라

728x90

wait와 notify를 꼭 사용해야 할 이유가 줄었다. 

 

- 자바 5에서 도입된 고수준의 동시성 유틸리티가 wait, notify를 직접 사용해서 하던 일들을 대신 처리해줌(java.util.concurrent)

 

- 실행자 프레임워크 (Executors)

- 동시성 컬렉션(concurent collection)

 

- List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 부여한 것 

- 각자 내부에서 동기화를 수행하므로 동시성을 무력화 하는 것이 불가능하고 외부에서 Lock를 사용하면 오히려 속도가 느려진다.

- 여러 메서드를 원자적으로 묶어서 호출하는 것 또한 불가능하다.

- 여러 기본 동작을 하나의 원자적 동작으로 묶는 상태 의존적 수정 메소드들이 추가되었다. (Java 8 default method 형태로 추가됨)

 

Map

default V putIfAbsent(K key,
                      V value){
//If the specified key is not already associated with a value (or is mapped to null) associates it with the given value and returns null, else returns the current value.
//Implementation Requirements:
//The default implementation is equivalent to, for this map:

   V v = map.get(key); // get => null  check => put 을 하나의 원자적 메소드로 묶음
   if (v == null)
       v = map.put(key, value);

   return v;
 }

String.intern 메서드는 String Constant Pool에 해당 문자열이 존재하면 반환하고 없다면 등록 후 반환하는 메서드이다.

 

ConcurrentMap putIfAbsent를 활용한버전의 String.intern(책에서는 6배 더 빠르다고 나옴)

Collections.synchronizedMap 보다는 ConcurrentHashMap를 사용하는 것이 좋다.

동기화된 맵보다 동시성 맵을 사용하는 것만으로 성능이 개선된다.

 

 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; // 원래 없었다면 null
    }

Queue

 

BlockingQueue(작업이 성공적으로 완료될떄까지 기다린다.(흐름을 차단한다.)

BlockingQueue.take(첫번째 원소를 꺼낸다. 단 큐가 비어있다면, 새로운 원소가 추가될떄까지 기라딘다. (생산자 -소비자 패턴에 적합하다. 소비자에서 큐가 비어있는걸 따로 처리해줄필요가 없기 때문에)

ThreadpoolExecutor등 대부분의 실행자 패턴에서 BlockingQueue를 사용해서 구현한다.

예제 : javabom.tistory.com/85

 

동기화 장치(synchronizer)

스레드끼리의 동기화를 조율해주는 장치

 

CountDownLatch

- 특정 동작(메소드 등)이 N번 수행되기 전까지 한 개 이상의 스레드를 붙잡아놓는다.

- await -> 생성자로 넘겨준  횟수만큼의 countDown이 호출되기전까지 스레드 실행을 막아놓음

 

 void countdown() throws InterruptedException {
        final int concurrency = 3;
        ExecutorService executorService = Executors.newFixedThreadPool(3); // 스레드풀에 스레드가 동시성 수준 이상만큼 있어야한다. 

        CountDownLatch ready = new CountDownLatch(concurrency); // 동시성 수준
        CountDownLatch start = new CountDownLatch(1); // 시작 방아쇠
        CountDownLatch done = new CountDownLatch(concurrency);

        for (int i = 0; i < concurrency; i++) {
            executorService.execute(() -> {
                ready.countDown(); // 준비
                try {
                    start.await(); //26번쨰 라인에서 countDown 1번 호출될떄 다시 시작
                    System.out.println("작업을 수행한다");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown(); // 작업 끝나면 done Countdown --> 작업이 총 세개가 동시니까 수를 일치시켜
                }
            });
        }

        ready.await(); // 동시성 수준(3)만큼 countDown이 호출될떄까지(준비될 때까지) 기다림
        long startNanos = System.nanoTime(); // 준비되고나서
        start.countDown(); // 준비됐으니까 start CountDown줌
        done.await();

        System.out.println(System.nanoTime() - startNanos); 
        //System.nanoTime()가 System.currentTimemillis보다 정확하고 시스템의 실시간 시계 시간 보정에 영향 받지 않는다.
    }

그럼에도 레거시 코드에서 wait, notify를 꼭 써야만 하는 경우.

일반적으로 notify보다는 notifyAll을 사용하는 것이 안전하며, wait는 항상 while문 내부에서 호출하도록 해야 한다.

 

- notify는 waitset에 있는 랜덤하게 스레드 하나를 꺠우고 notifyAll은 스레드를 전부다 깨우고 그 중하나만이 락을 획득한다.

(notify로 인해 전혀 관계 없는 쓰레드가 깨어났는데 별 동작없이 그냥 가버린다면 기존 스레드들은 영원히 잠잘것이다)

synchronized (obj){
   while (조건이 충족되지 않았다){
     obj.wait()
   }
 }