책/이펙티브 자바
item 79 과도한 동기화는 피하라
함께자라기
2022. 4. 23. 11:58
ITEM 79 과도한 동기화는 피하라
과도한 동기화는 성능을 떨어뜨리고 교착상태에 빠뜨리고 예측할 수 없는 동작을 낳기도 한다
- 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다
- 동기화된 영역 안에서는 재정의할 수 있는 메서드는 호출하면 안된다
- 클라이언트가 넘겨준 함수 객체를 호출해서도 안된다
- 동기화된 영역을 포함한 클래스 관점에서는 이런 메서드는 다른 차원에서 온 외계인이다
- 그 메서드가 무슨 일을 할지 알지 못하며 통제할 수도 없다는 뜻이다
- 외계인 메서드(alien method)가 하는 일에 따라 동기화된 영역은 예외를 일으키거나 교착상태에 빠지거나 데이터를 훼손할 수도 있다
열린 호출(open call)
- 동기화 영역 바깥에서 호출되는 외계인 메서드를 말한다
- 외계인 메서드는 얼마나 오래 실행될지 알 수 없는데 동기화 영역 안에서 호출 된다면 그동안 다른 스레드는 보호된 자원을 사용하지 못하고 대기 해야만 한다
- 따라서 열린 호출은 실패방지 효과 외에도 동시성 효율을 크게 개선해준다
기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다
- 락을 얻고 공유 데이터를 검사하고 필요하면 수정하고 락을 놓는다
- 오래 걸리는 작업이라면 item 78의 지침을 어기지 않으면서 동기화 영역 바깥으로 옮기는 방법을 찾아보자
성능에 관한 측면
- 자바 동기화 비용은 빠르게 낮아져 왔지만 과도한 동기화를 피하는 일은 어느때보다 중요하다
- 멀티코어가 일반화된 시기인 만큼 과도한 동기화가 초래하는 진짜 비용은 락을 얻는 데 드는 CPU 시간이 아니다
- 경쟁하느라 낭비하는 시간, 병렬로 실행할 기회를 잃고 모든 코어가 메모리를 일관되게 보기 위한 지연시간이 진짜 비용이다
- 가상머신의 코드 최적화를 제한한다는 점도 과도한 동기화의 또 다른 숨은 비용이다
가변 클래스를 작성하려거든 다음 두 선택지 중 하나를 따르자
- 동기화를 전혀 하지 말고 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자
- java.util은 Vector와 Hashtable을 제외하고 첫번째 방식을 택했다
- 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자
- 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두 번째 방법을 선택해야 한다
- java.util.concurrent는 두번째 방식을 택했다
- 자바도 초창기에는 이 지침을 따르지 않은 클래스가 많았다
- StringBuffer 인스턴스는 거의 항상 단일 스레드에서 쓰였지만 내부적으로 동기화를 수행해서 뒤늦게 StringBuilder가 등장했다
- StringBuilder는 그저 동기화 하지 않은 StringBuffer다
- 비슷한 이유로 스레드 안전한 의사 난수 발생기인 java.util.Random은 동기화하지 않은 버전인 java.util.concurrent.ThreadLocalRandom으로 대체되었다
- 선택하기 어렵다면 동기화하지 말고 문서에 "스레드 안전하지 않다"고 명시하자
클래스를 내부에서 동기화하기로 했다면?
- 락분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 동원해 동시성을 높여줄 수 있다
여러 스레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면 그 필드를 사용하기 전에 반드시 동기화해야 한다
- 비결정적 행동도 용인하는 클래스라면 상관없다