→ 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다!
ex) 동기화된 영역 안에서는 재정의할 수 있는 메서드를 호출하지 말자, 클라이언트가 넘겨준 함수 객체를 호출하지 말자
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);
}
}
private void notifyElementAdded(E element) { // Set에 add하면 관찰자의 added 메서드를 호출한다.
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); // notifyElementAdded를 호출한다.
return result;
}
}
어떤 집합(Set)을 감싼 래퍼 클래스이고, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있음 (관찰자 패턴)
관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지
두 경우 모두 다음 콜백 인터페이스의 인스턴스를 메시지에 건넴
@FunctionalInterface public interface SetObserver<E> {
// ObservableSet에 원소가 더해지면 호출된다.
void added(ObservableSet<E> set, E element);
}
→ 위의 커스텀 함수형 인터페이스를 정의한 이유는 이름이 더 직관적이고 다중 콜백을 지원하도록 확장할 수 있기 때문
observableSet을 사용한 main 메서드 예
ex1)
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(New HashSet<>());
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) s.removeObserver(this);
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
ex2)
set.addObserver(new SetObserver<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) {
ExecutorService exec = Executors.newSingleThreadExecutor();
try {
exec.submit(() -> s.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException ex) {
throw new AssertionError(ex);
} finally {
exec.shutdown();
}
}
}
});
구독 해지하는 관찰자를 작성하는데 removeObserver를 직접 호출하는 것이 아니라, 다른 스레드에게 넘김
예외는 발생하지 않지만 교착상태에 빠지게 됨
→ 백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 하지만 락을 얻을 수 없음(메인 스레드가 락을 가지고 있음) +메인 스레드는 백그라운드 스레드가 관찰자를 제거하기를 기다림
해결방법
메서드 호출을 동기화 블록 바깥으로 옮기기
private void notifyElementAdded(E element) {
List<SetObserver<E>> snapshot = null;
synchronized(observers) {
snapshot = new ArrayList<>(observers);
}
for (SetObserver<E> observer : snapshot)
observer.added(this, element);
}
→ synchronized를 리스트를 복사하는 부분으로 제한하고, 실제 observer.added 메서드 호출은 동기화 블록 밖에서 이루어짐
→ observers에 대한 동기화가 최소화되면서, 다른 스레드가 observers 리스트를 수정하는데 영향을 주지 않게 됨
→ snapshot 복사본을 사용해서, 리스트가 변경되더라도 이미 복사된 데이터를 사용하므로, 충돌 방지
java.util.concurrent.CopyOnWriteArrayList 사용
private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();
public void addObserver(SetObserver<E> observer) {
observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) {
return observers.remove(observer);
}
private void notifyElementAdded(E element) {
for (SetObserver<E> observer : observers)
observer.added(this, element);
}
→ notifyElementAdded 메서드에서 관찰자 리스트를 복사해서 사용 (쓰기 작업이 일어날 때마다)
⇒ 예외 발생과 교착 발생 모두 사라짐