728x90
과도한 동기화는 피하라
응답불가와 안전 실패를 피하려면 동기화 메서드나 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
ObservableSet클래스
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
public class ObservableSet<E> extends ForwardingSet<E> {
public ObservableSet(Set<E> s) {
super(s);
}
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);
}
}
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;
}
}
예제 1
삭제하는 순간에 객체의 List(observers)를 순회하고 있기 때문에 에러를 발생시킨다(ConcurrentModificationException).
public static void main(String[] args) {
ObservableSet<Integer> set =
new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if(element == 23)
set.removeObserver(this);
}
});
for(int i = 0; i < 100; ++i)
set.add(i);
}
예제 2
메인스레드가 이미 락을 쥐고 있는 상태이므로 백그라운드 스레드는 removeObserver를 수행하기 위하 락을 잡을 수 없다. 동시에 메인스레드는 객체를 제거하기만을 기다리고 있어 데드락 상태가 된다.
set.addObserver(new SetObserver<>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if(element == 23) {
ExecutorService exec =
Executors.newSingleThreadExecutor();
try {
exec.submit(() -> set.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException ex) {
throw new AssertionError(ex);
} finally {
exec.shutdown();
}
}
}
});
해결방법 1
자바 언어의 락은 재진입을 허용하므로 교착상태에 빠지지는 않는다. 하지만, 안전실패가 발생 할 수 있으므로 객체를 복사해서 쓴다면 해결 할 수 있다.
private void notifyElementAdded(E element) {
synchronized (observers) {
snapshot = new ArrayList<>(observers);
}
for (SetObserver<E> observer : snapshot) {
observer.added(this, element);
}
}
해결방법 2
CopyOnWriteArrayList가 정확히 이목적으로 특별히 설계된 것이다. 항상 깨끗한 복사본을 만들어 락이 필요없어 매우 빠르다.
*하지만 다른 용도로 쓰이면 끔찍하게 느리다.
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();
public void addObserver(SetObserver<E> observer) {
synchronized (observers) {
observers.add(observer);
}
}
public boolean removeObserver(SetObserver<E> observer) {
synchronized (observers) {
return observers.remove(observer);
}
}
기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다(ITEM78).
가변 클래스를 작성하려거든 다음 두 선택지 중 하나를 따르자.
- 동기화를 전혀 하지 말고 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자.
- 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자(ITEM 82).
728x90
댓글