티스토리 뷰

코드를 새로 작성한다면 wait와 notify를 쓸 이유가 없다.

💻 java.util.concurrent

자바 5에서 도입된 고수준의 동시성 유틸리티 덕분에

새로 작성하는 코드에서는 wait와 notify는 거의 쓸 일이 없어졌다.

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

 

🤷‍♂️ 동시성 컬렉션 (concurrent collection)

동시성 컬렉션은 List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다.

높은 동시성에 도달하기 위해 동기화를 각자의 내부에서 수행하므로,

동시성 컬렉션에서 동시성을 무력화하는 건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도라 느려진다.

 

동시성 컬렉션에서 동시성을 무력화하지 못하므로 여러 메서드를 원자적으로 묶어 호출하는 일 역시 불가능하다.

따라서 여러 기본 동작을 하나의 원자적 동작으로 묶는 '상태 의존적 수정' 메서드들이 추가되었다

이 메서드들은 유용해서 자바 8에서는 일반 컬렉션 인터페이스에도 디폴트 메서드 형태로 추가되었다.

 

예를 들어, putIfAbsent는 Map의 디폴트 메서드인데,

인자로 넘겨진 key가 없을 때 value를 추가한다.

기존 값이 있으면 그 값을 반환하고 없는 경우에는 null을 반환한다.

String.intern의 동작을 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;
}

ConcurrentHashMap은 동시성이 뛰어나며 속도도 무척 빠르다.

따라서 이제는 Collections.synchronizedMap보다는 ConcurrentHashMap을 사용하는 것이 훨씬 좋다.

동기화된 맵을 동시성 맵으로 교체하는 것만으로 동시성 애플리케이션의 성능은 극적으로 개선된다.

 

🤷‍♀️ 동기화 장치

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

 

대표적인 동기화 장치로는 CountDownLatch와 Semaphore, Phaser가 있다.

 

예를 들어, 어떤 동작들을 동시에 시작해 모두 완료하기까지의 시간을 재는 간단한 프레임워크를 구축한다고 해보자.

wait와 notify만으로 구현하려면 아주 난해하고 지저분한 코드가 탄생하지만, CountDownLatch를 쓰면 직관적으로 구현할 수 있다

//동시 실행 시간을 재는 간단한 프레임워크
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;
}

여기서 time 메서드에 넘겨진 실행자는 concurrency 매개변수로 지정한 동시성 수준만큼의 스레드를 생성할 수 있어야 한다.

그렇지 못하면 이 메서드는 결코 끝나지 않을 것이다.

이런 상태를 스레드 기아 교착상태라 한다.

※ 시간 간격을 잴 때는 항상 System.currentTimeMillis가 아닌 System.nanoTime을 사용하자.

 

📢 wait & notify

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

하지만 어쩔 수 없이 레거시 코드를 다뤄야 할 때도 있을 것이다.

wait메서드는 스레드가 어떤 조건이 충족되기를 기다리게 할 때 사용한다.

wait 메서드를 사용할 때는 반드시 대기 반복문( wait loop ) 관용구를 사용하자.

반복문 밖에서는 절대로 호출하면 안 된다.

 

또한 notify와 notifyAll 중 무엇을 선택해야 할지 선택할 땐 

일반적으로 notifyAll을 사용하는 게 합리적이고 안전하다.

외부로 공개된 객체에 대해 실수로 혹은 악의적으로 notify를 호출하는 상황에 대비하기 위해 wait를 반복문 안에서 호출했듯, notify대신 notifyAll을 사용하면 관련 없는 스레드가 실수로 혹은 악의적으로 wait을 호출하는 공격으로부터 보호할 수 있다.

 

💡 정리

  • 코드를 새로 작성한다면 wait와 notify대신 concurrent 패키지를 사용하자.
  • wait와 notify를 사용한 레거시 코드를 유지보수해야 한다면 wait은 항상 표준 관용구에 따라 while문 안에서 호출하도록 하자.
  • 일반적으로 notify보다는 notifyAll을 사용해야 한다.
  • 혹시라도 notify를 사용한다면 응답 불가 상태에 빠지지 않도록 각별히 주의하자.
댓글
공지사항
최근에 올라온 글