[Effective Java 3/e] 아이템 79 - 과도한 동기화는 피하라


아이템 79 - 과도한 동기화는 피하라

주의사항

  • 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다.
  • 외계인 메서드(alien method) - 외부에서 제어 가능한 메서드
    • 동기화된 영역 안에서 재정의할 수 있는 메서드
    • 클라이어트가 넘겨준 함수 객체

잘못된 코드 - 동기화 블록 안에서 외계인 메서드를 호출한다

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) {
        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;
    }
}

@FunctionalInterface
public interface SetObserver<E> {
    //ObservableSet에 원소가 더해지면 호출된다.
    void added(ObservableSet<E> set, E element);
}

  • 관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지

예제 1 - 정수값이 23이면 자기 자신을 제거하는 관찰자

public static void main(String[] args) {
    ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
    set.addObserver(new SetObserver<>() {
        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);
    }
}
  • 0~23까지 출력한 후 관찰자 자신을 구독해지한 다음 종료할 것으로 예상
    • 23까지 출력한 다음 ConcurrentModificationException 던짐
      • 관찰자의 added 메서드 호출이 일어난 시점 - notifyElementAdded가 관찰자들의 리스트를 순회하는 중
      • added 메서드는 ObservableSet의 removeObserver 메서드 호출 -> observers.remove 메서드 호출
        • 리스트에서 원소를 제거하려 하는데 리스트를 순회 중이므로 ConcurrentModificationException 발생

예제 2 - 쓸데없이 백그라운드 스레드를 사용하는 관찰자

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.submit(() -> s.removeObserver(this)).get(); //메인 스레드가 락을 쥐고 있어서 락을 얻을 수 없다.
              //메인 스레드는 백그라운드 스레드가 관찰자를 제거하기만을 기다리고 있다.
            } catch(ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                exec.shutdown();
            }
        }
    }
});
  • 교착상태 발생

교착상태 해결방법

외계인 메서드를 동기화 블록 바깥으로 옮겼다

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);
    }
}
  • 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게 순회 가능
  • 예외 발생과 교착상태 증상 해결 가능

더 나은 방법 - CopyOnWriteArrayList를 사용해 구현한 스레드 안전하고 관찰 가능한 집합

private final List<SetObserser<E>> observers = new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> observer) {
    observers.add(observer);
}

public boolean removeObserver(SetObserver<E> observer) {
    return observers.remove(observer);
}

public void notifyElementAdded(E element) {
    for (SetObserver<E> observer : observers) {
         observers.added(this, element);
    }
}
  • 내부를 변경하는 작업은 깨끗한 복사본을 만들어 수행하도록 구현 됨
  • 내부의 배열은 절대 수정되지 않으니 순회할 때 락이 필요 없어 매우 빠름

정리

  • 기본 규칙 - 동기화 영역에서는 가능한 일을 적게 하라!
  • 교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말 것!