• 이전 장 아이템 78에서는 동기화가 충분히 이루어지지 못한 경우를 다뤄봤다.
  • 이번 챕터는 과도하게 동기화의 경우를 다룬다.
    • 과도한 동기화는 성능을 떨어트리고, 교착상태에 빠트리고, 예측할 수 없는 동작을 낳기도 한다.

 

응답 불가와 안전 실패를 피하려면 동기화 메소드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.

 

 

  • 동기화 된 영역에서 재정의하는 메소드가 호출되거나 클라이언트로부터 함수 객체가 넘어와서도 안된다.
  • 그렇게 들어온 메소드나 객체는 통제할 수 없기 때문에 예외를 일으키거나 교착상태에 빠지거나 데이터를 훼손할 수 있다.
  • 관찰자 패턴을 통해 예시를 봐보자.
// 어떤 집합(Set)을 감싼 래퍼 클래스, 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있음

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(observes) {
            for (SetObserver<E> observer : observers)
                observer.added(this, element);
        }
    }

    @Override public boolean add(E elemnet) {
        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;
    }
}
  • 관찰자들은 addObserver와 removerObserver 메소드를 호출해 구독을 신청하거나 해지한다.
  • 두 경우 모두 다음 콜백 인터페이스의 인스턴스를 메소드에게 건낸다.
@FunctionalInterface public interface SetObserver<E> {
    // ObservableSet에 원소가 더해지면 호출된다.
    void added(ObservableSet<E> set, E element);
}
  • 그냥 얼핏 봤을 때는 문제가 없어보인다.
//구독자를 99까지 늘리는 작업
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);
    }


    // 자기 자신(23)이 되었을 때 삭제를 진행하는 메소드
    set.addObserver(new SetObserver<>() {
       public void added(ObservableSet<Integer> s, Integer e) {
           System.out.println(e);
           if (e == 23)
               s.removeObserver(this);
       } 
    });
} 
  • 0부터 23까지 출력 한 후 자기 자신을 삭제하며 끝날 것 같지만 ConcurrentModificationException을 던진다.
  • added 메소드 호출이 일어난 시점이 notifyElementAdded가 관찰자들의 리스트를 순회하는 도중이기 때문이다.
  • added 메소드는 ObservableSet의 removeObserver를 호출하고 이 메소드는 다시 observer.remove를 호출한다.
    • 여기서 문제가 일어나는데 삭제하는 도중 리스트 순회가 이루어지고 있기 때문이다.
    • 결국 문제는 동기화된 블록 안에서 너무 여러 작업이 이루어지고 있다는 점이다.
      • 이 결과로 콜백이 꼬이게 되는것

 

이런 문제는 그래서 메소드 호출을 동기화 블록 바깥으로 옮기면 된다.

 

 

  • notifyElementAdded 메소드에서라면 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게순회할 수 있다.
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가 정확히 이 목적을 위해 특별히 설계되었다.
      • ArrayList를 구현한 클래스로 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현되었다.
      • 순회용으로 최적이다.
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);
}
  • 아까 위에서 처럼 메소드를 동기화 블록 바깥으로 뺀 메소드를 열린 호출(open call)이라 한다.

동기화에서 가장 중요한 것은 낭비되는 시간, 즉 병렬로 실행할 기회를 읽고 모든 코어가 메모리를 일관되게 보기 위한 지연시간이 진짜 비용이다.

  • 가변 클래스를 작성할 때 두가지를 명심하자.
    • 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자
    • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자.
      • 단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두번째 방법을 선택해야 한다.

+ Recent posts