<aside> 🚨 충분하지 못한 동기화도 문제이지만 과도한 동기화도 문제다. 과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리고, 심지어 예측할 수 없는 동작을 낳기도 한다.
</aside>
<aside> 📚 응답 불가와 안전 실패를 피하려면 동기화 메서드/블록 안에서는 제어권을 클라이언트에게 양도하면 안된다.
</aside>
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> set) { super(set); }
private final List<SetObserver<E>> observers = new ArrayList<>();
public void addObserver(SetObserver<E> observer) {
synchronized(observers) { observers.add(observer); }
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized(observers) { return observers.remove(observer); }
}
// 외계인 메서드 호출 부분
// observer의 added : 동기화된 영역에서 재정의할 수 있는 메서드(제어권이 클라이언트에 있다)
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;
}
@Override
public boolean addAll(Collection<? extends E> c) {
boolean result = false;
for (E element : c) result |= add(element);
return result;
}
}
public class Main {
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((s, e) -> System.out.println(e));
for (int i = 0; i < 100; i++) set.add(i);
}
}
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) set.removeObserver(this); // 문제 발생
}
});
ConcurrentModificationException
을 던진다.added
메서드 호출이 일어난 시점이 notifyElementAdded
가 관찰자들의 리스트를 순회하는 도중이기 때문이다.
added
메서드 : ObservableSet의 removeObserver
메서드 호출removeObserver
메서드 : observers.remove
메서드 호출
notifyElementAdded
메서드에서 수행하는 순회는 동기화 블록 안에 있어서 동시 수정이 일어나지 않음을 보장하지만,
정작 자신이 콜백을 거쳐 되돌아와 수정하는 것까지 막지는 못한다.set.addObserver(new SetObserver<>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) {
ExecutorService exec = Executors.newSingleThreadExecutor();
try {
exec.sumbit(() -> s.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException ex) {
throw new AssertionError(ex);
} finally {
exec.shtdown();
}
s.removeObserver(this);
}
}
});
removeObserver
를 직접 호출하지 않고, 실행자 서비스를 사용해 다른 스레드에 구독 해지를 부탁해보자