67. 과도한 동기화는 피하라.
과도한 동기화 시 발생할 수 있는 문제
- 교착상태(deadlock)
- 비결정적 동작(nondeterministic behavior)
동기화 메서드나 블록 안에서 클라이언트에게 프로그램 제어 흐름을 넘기지 마라.
다시 말해서, 동기화가 적용된 영역(synchronized) 안에서는 재정의 가능 메서드나 클라이언트가 제공한 함수 객체 메서드를 호출하지 말라.
동기화 영역이 존재하는 관점에서 보면, 그런 메서드는 불가해(alien)메서드다. 무슨 일을 하는지 알수도 없고 제어할 수도 없다. 불가해 메서드가 어떤 일을 하냐에 따라, 동기화 영역 안에서 호출하게 되면 예외나 교착 상태, 데이터 훼손(data corruption)이 발생 할 수 있다.
불가해 메서드 오류 예제
public interface SetObserver<E>{
// 구독자 집합에 새 원소가 추가되었을 때 호출됨
void added(ObservableSet<E> set, E element);
}
// 동기화 블록안에서 불가해 메서드를 호출하는 잘못된 사례!
public class ObservableSet<E> extends ForwardingSet<E> {
private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>>();
public void addObserver(SetObserver<E> observer){
synchronized (observers){
observers.add(observer);
}
}
private void notifyElementAdded(E element) {
synchronized(observers) {
for (SetObserver<E> observer : observers) {
observer.added(this, element);
}
}
}
@Override
public boolean add(E element){
boolean added = super.add(element);
if(added)
notifyElementAdded(element);
return added;
}
}
/**
* 실제 동작시, 0 ~ 23까지 출력 후 ConcurrentModificationException 발생함
*/
public static void main(String[] args){
ObservableSet<Integer> set = new ObservaleSet<Integer>(new HashSet<Integer>);
set.addObserver(new SetObserver<Integer>(){
public void added(ObservableSet<Integer> s, Integer e){
System.out.println(e);
if(e == 32)
s.removeObserver(this);
}
});
for( int i = 0; i < 100; ++i ){
set.add(i);
}
}
위의 코드는 리스트를 순회 하고 있는 중(notifyElementAdded
)에 원소를 삭제 하려 시도 하기 때문에 오류가 발생한다.
notifyElementAdded 메서드의 순환문은 동기화 블럭 안에 있다. observers리스트가 병렬적으로 수정되는 일을 막기 위해서였다.
하지만, 이렇게 했어도 순환문을 실행하는 스레드 자신이 구독자 집합에 저장된 메서드(```SetObserver.added``)를 역호출(callback)해서
observers리스트를 변경하는 경우까지 차단 할수는 없다.
불가해 메서드 오류 해결 방안
- 방어적 복사를 사용
private void notifyElementAdded(E element) { List<SetObserver<E>> snapshot = null; synchronized(observers) { snapshot = new ArrayList<SetObserver<E>>(observers); } for (SetObserver<E> observer : snapshot) { observer.added(this, element); } }
- ConcurrentCollection사용
```java
// 다중 스레드에 안전한 구독자 집합 : CopyOnWriteArrayList 이용
private final List<SetObserver
> observers = new CopyOnWriteArrayList<SetObserver >();
public void addObserver(SetObserver
명심해야 할 것
- 동기화 영역 안에서 수행되는 작업의 양을 가능한 줄여야 한다.
- 멀티코어 세상에서 동기화의 진짜 비용은 락을 거느라 소비되는 CPU시간이 아니다. 병렬성을 활용한 기회를 잃는 다는 것, 그리고 모든 코어가 동일한 메모리 뷰를 보도록 하기 위해 피룡한 지연시간이 더 큰 비용이다.
- static 필드(또는 단일객체 내 필드)를 변경하는 메서드가 있을 때는 해당 필드에 대한 접근을 반드시 동기화 해야 한다.
결론
- 기화 영역 안에서 불가해(alien) 메서드를 호출하지 않는다.
- 동기화 영역 안에서 작업을 제한(최소화) 한다.
- 가변(변경 가능) 클래스 내 동기화가 필요한지 검토하고 필요하다면 동기화 한다.