@Override publicbooleanaddAll(Collection<? extends E> c) { booleanresult=false; for (E element : c) { result |= add(element); //notifyElementAdded를 호출 } return result; } }
백그라운드 스레드가 s.removeObserver를 호출하면 Observer를 잠그려 시도하지만 락을 얻을 수 없다. (메인스레드가 이미 락을 쥐고 있기 때문 - removeObserver는 synchronized 키워드가 달려있어서 실행 시 락이 걸린다.)
그와 동시에 메인 스레드는 백그라운드 스레드가 Observer를 제거하기만을 기다리는 중이다.
교착상태 해결방법
자바 언어의 락은 재진입(reentrant)을 허용하므로 교착상태에 빠지지는 않는다. 재진입 가능 락은 객체 지향 멀티스레드 프로그램을 쉽게 구현 할 수 있도록 해준다. 하지만 응답 불가(교착상태)가 될 상황을 안전 실패(데이터 훼손)으로 변모시킬 수도 있다.
이런 경우 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 된다.
1 2 3 4 5 6 7 8 9
privatevoidnotifyElementAdded(E element) { List<SetObserver<E>> snapshot = null; synchronized(observers) { snapshot = newArrayList<>(observers); } for (SetObserver<E> observer : snapshot) { observer.added(this, element); //외계인 메서드를 동기화 블록 바깥으로 옮겼다. } }
CopyOnWriteArrayList
외계인 메서드 호출을 동기화 블록 바깥으로 옮기는 것 보다 더 나은 방법은 java.util.concurrent 패키지의 CopyOnWriteArrayList를 사용하는 것이 좋다. 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행한다. 내부의 배열은 절대 수정되지 않아 락이 없어 빠르다. 다른 용도로 쓰인다면 매번 복사해서 느리겠지만, 수정할 일은 드물고 순회만 빈번히 일어나는 Observer 리스트 용으로는 딱이다.
자바의 동기화 비용은 빠르게 낮아져 왔지만, 과도한 동기화를 피하는일은 오히려 과거 어느 때보다 중요하다. 멀티코어가 일반화된 오늘날 과도한 동기화가 초래하는 진짜 비용은 락을 얻는데 드는 CPU 시간이 아니다. 서로 스레드끼리 경쟁하는 Race Condition에 낭비가 발생한다.
병렬로 실행할 기회를 잃는다.
모든 코어가 메모리를 일관되게 보기위한 지연시간이 진짜 비용
가상머신의 코드최적화를 제한하는 점도 숨은 비용
가변 클래스를 작성하는 경우 동기화에 대해 고려할 점
동기화를 전혀 하지 말고 가변 클래스를 동시에 사용해야하는 클래스가 외부에서 동기화하자
동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자. (단 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두번째 방법을 쓴다)
정리
기본 규칙은 동기화 영역에서 가능한 한 일을 적게하는 것이다. (락을 얻고 공유 데이터를 검사하고, 필요하면 수정하고, 락을 놓는다.)
오래 걸리는 작업이라면 동기화 영역 밖으로 옮기는 방법을 찾아보자.
여러 스레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면 그 필드를 사용하기 전에 반드시 동기화 해야 한다.
교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자
동기화 영역 안에서 작업은 최소한으로 줄이자
가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자
지금은 과도한 동기화를 피하는게 제일 중요하다
합당한 이유가 있을때만 내부에서 동기화하고 동기화 여부를 문서에 남기자. (웬만하면 외부에서 동기화를 하자)
참고
Effective Java 3rd Edition - Item 79. 과도한 동기화는 피하라