JAVA/Effective Java

규칙 69 - wait나 notify 대신 병행성 유틸리티를 이용하라.

반응형

멀티 쓰레드 환경에서 wait와 notify를 사용할 경우에 주의가 필요하다. 하지만 그것을 효율적으로 사용하기에는 많은 어려움이 따른다.


그렇기 위해서 wait와 notify를 정확하게 사용하기 위해서는 high-level util(고수준 유틸리티)을 사용해야 한다.

이런 고수준 유틸리티들은 Executor, Concurrent Colloection, Synchronizer를 통해 사용할 수 있다.

그중 Conncurrent Collenction (병행 컬렉션)에 대해 알아보자. 대부분에 컬렉션 Map, List, Queue등은 병행 컬렉션을 제공한다. 



병행성 컬렉션

대표적으로 Map에서 제공하는 ConcurrentMap이다. 그중 putIfAbsent(key, value) 메서드가 대표적이다. 이 메서드는 키에 해당하는 값이 없을 경우에만 주어진 값을 넣고, 해당 키에 저장되어 있었던 기존의 값을 반환한다. 

속도도 일반 Map과 큰 차이가 없다. 그렇기 때문에 병행성 처리가 가능한 Map을 사용하도록 하자.



Wait와 notify, notifyAll 주의사항 및 사용법

wait를 통해 특정 스레드가 대기를 하고자 할 때는 다음과 같이 표준적 숙어를 사용해야 한다. (일반적으로 만들어진 규칙)

1
2
3
4
5
6
// wait 메서드를 사용하는 표준적 숙어
synchronized (obj) {
  while (<조건이 만족 되지 않을경우 순환문 실행>)
     obj.wait();
 
}
cs

왜냐하면 조건이 만족되지 않았는데 wait가 호출되어 스레드가 대기하게 되고 잘못된 코드로 인해 notify가 호출된 뒤에 wait가 호출된다거나 하는 경우에 영원히 깨어나지 못하는 생존오류가 발생할 수 있기 때문이다.


그래서 다른 스레드를 깨울때 notify 메서드를 사용하지만 notifyAll 메서드를 사용해서 일어나지 못한 다른 스레드를 깨워주는 것이 좋다. (notify 보다 무조건 notifyAll을 사용하라!)

하지만 그럴경우에 깨어날 필요가 없는 쓰레드를 깨우지만 프로그램의 정확성에는 영향을 끼치지 않는다. 왜냐면 위에 코드에서 while문에 보면 알겠지만 만약 조건이 만족이 되지 않으면 본인이 알아서 다시 wait 상태로 돌아가기 때문이다.


-> wait 사용시 숙어를 사용하고 notify대신 notifyAll을 사용하여 일어나지 못한 스레드를 깨워줘라.



시간 체크 메서드

마지막으로 병행성 처리에서 시간은 굉장히 예민하기 때문에 시간값이 필요로 할때는 System.currentTimeMillis 대신 System.nanoTime을 사용해야 한다.


두 개의 메소드를 비교한 내용을 보자.

  • currentTimeMillis()
    • 날짜와 관련한 시각 계산을 위해 사용(ms 단위)
    • 시스템 시간을 참조(외부데이터)
    • 일반적인 업데이트 간격은 인터럽트의 타이머(10microsec)에 따름
    • GetSystemTimeAsFileTime 메서드로 구현되어 있으며, 운영체제가 관리하는 시간값을 가져옴
  • nanoTime()
    • 기준 시점에서 경과 시간을 측정하는데 사용(ns 단위)
    • 시스템 시간과 무관
    • QueryPerformanceCounter/QueryPerformanceFrequency API로 구현되어 있음.

그렇기 때문에 정확한 시간 계산을 위해서 nanoTime()을 사용해야 한다.




정리하자면 병행 콜렉션과 같은 고수준 유틸리티를 사용하면 wait, notify를 직접적으로 사용할 이유가 많이 줄어들고 안정적으로 코딩할 수 있다. 


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙69

반응형