-
item 81 wait와 notify보다는 동시성 유틸리티를 애용하라책/이펙티브 자바 2022. 4. 25. 11:46
ITEM 81 wait와 notify보다는 동시성 유틸리티를 애용하라
wait와 notifiy는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자
- java.util.concurrent의 고수준 유틸리티는 세 범주로 나눌 수 있다
- 실행자 프레임워크, 동시성 컬렉션(concurrent collection), 동기화 장치(synchronizer)
- 실행자 프레임워크는 item80에서 가볍게 알아 보았고 이번 item에서는 동시성 컬렉션과 동기화 장치를 알아본다
동시성 컬렉션
- List, Queue, Map 같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션이다
- 높은 동시성에 도달하기 위해 동기화를 각자의 내부에서 수행한다
- 동시성 컬렉션에서 동시성을 무시력화하는 건 불가능하며 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다
- 동시성 컬렉션에서 동시성을 무력화하지 못하므로 여러 메서드를 원자적으로 묶어 호출하는 일도 불가능하다
- 따라서 여러 기본 동작을 하나의 원자적 동작으로 묶는 상태 의존적 수정 메서드들이 추가되었다
- 이 메서드들은 아주 유용하여 java8에서는 일반 컬렉션의 인터페이스에도 디폴트 메서드 형태로 추가되었다
wait 메서드를 사용할 때는 반드시 대기 반복문(wait loop) 관용구를 사용하라. 반복문 밖에서는 절대로 호출하지 말자
wait 메서드를 사용하는 표준 방식 synchronized (obj) { while (<조건이 충족되지 않았다>) obj.wait(); // 락을 놓고 깨어나면 다시 잡는다 ... // 조건이 충족됐을 때 동작을 수행한다 }
- 이 반복문은 wait 호출 전후로 조건이 만족하는지 검사하는 역할을 한다
- 대기 전에 조건을 검사하여 조건이 이미 충족되었다면 wait를 건너뛰게 한것은 응답 불가 상태를 예방하는 조치다
- 조건이 이미 충족되었는데 스레드가 notify(또는 notifyAll) 메서드를 먼저 호출한 후 대기 상태로 빠지면 그 스레드를 다시 깨울 수 있다고 보장할 수 없다
- 대기 후에 조건을 검사하여 조건이 충족되지 않았다면 다시 대기하게 하는것은 실패를 막는 조치다
- 조건이 충족되지 않았는데 스레드가 동작을 이어가면 락이 보호하는 불변식을 깨뜨릴 위험이 있다
조건이 만족되지 않아도 스레드가 깨어날 수 있는 몇가지 상황
- 스레드가 notify를 호출한 다음 대기 중이던 스레드가 깨어나는 사이에 다른 스레드가 락을 얻어 그 락이 보호하는 상태를 변경한다
- 조건이 만족되지 않았음에도 다른 스레드가 실수, 악의적으로 nofity를 호출한다
- 공개된 객체를 락으로 사용해 대기하는 클래스는 이런 위험에 노출된다
- 외부에 노출된 객체의 동기화된 메서드 안에서 호출하는 wait는 모두 이 문제에 영향을 받는다
- 깨우는 스레드는 지나치게 관대해서 대기 중인 스레드 중 일부만 조건이 충족되어도 notifyAll을 호출해 모든 스레드를 깨울 수도 있다
- 대기 중인 스레드가 드물게 notify 없이도 깨어나는 경우가 있다
- 허위 각성(spurious wakeup)이라는 현상
notify와 notifyAll 중 무엇을 선택하느냐
- notify는 스레드 하나만 깨우며 notifyAll은 모든 스레드를 깨운다
- 일반적으로는 언제나 notifyAll을 사용하는게 합리적이고 안전한 방법이다
- 깨어나야 하는 모든 스레드가 깨어남을 보장하기 때문에 항상 정확한 결과를 얻는다
- 다른 스레드까지 깨어날 수도 있지만 그건 여러분의 프로그램에 영향을 주지 않을 것이다
- 깨어난 스레드들은 기다리던 조건이 충족되었는지 확인하여 충족되지 않았다면 다시 대기할 것이다
- 모든 스레드가 같은 조건을 기다리고 조건이 한 번 충족될 때마다 단 하나의 스레드만 혜택을 받을 수 있다면 notifyAll 대신 notify를 사용해 최적화가 가능하다
- 위 전제 조건들이 만족된 상황이라도 notify 대신 notifyAll을 사용해야 하는 이유가 있다
- 외부로 공개된 객체에 대해 실수, 악의적으로 notify를 호출하는 상황에 대비하기 위해 wait를 반복문 안에서 호출했듯 notify 대신 notifyAll을 사용하면 관련 없는 스레드가 실수로 혹은 악의적으로 wait를 호출하는 공격으로부터 보호할 수 있다
- 그런 스레드가 중요한 notify를 삼켜버린다면 꼭 깨어났어야 할 스레드들이 영원히 대기하게 될 수 있다
wait와 notify를 직접 사용하는 것을 동시성 어셈블리 언어로 프로그래밍하는 것에 비유할 수 있다
반면 java.util.concurrent는 고수준 언어에 비유할 수 있다
코드를 새로 작성한다면 wait와 notify를 쓸 이유가 거의 없다
이들을 사용하는 레거시 코드를 유지보수해야 한다면 wait는 항상 표준 관용구에 따라 while문 안에서 호출하도록 하자
일반적으로 notify보다는 notifyAll을 사용해야 하며 혹시 notify를 사용한다면 응답 불가 상태에 빠지지 않도록 주의하자
'책 > 이펙티브 자바' 카테고리의 다른 글
item 83 지연 초기화는 신중히 사용하라 (0) 2022.04.25 item 82 스레드 안정성 수준을 문서화하라 (0) 2022.04.25 item 80 스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) 2022.04.23 item 79 과도한 동기화는 피하라 (0) 2022.04.23 item 78 공유 중인 가변 데이터는 동기화해 사용하라 (0) 2022.04.23 - java.util.concurrent의 고수준 유틸리티는 세 범주로 나눌 수 있다