79. 과도한 동기화는 피하라.

  • 과도한 동기화는 성능저하, 교착상태, 예측할 수 없는 동작의 원인이 된다.

  • 동기화 메서드나 동기화 블록 안에서 제어를 클라이언트에게 양보하지 말자.

      • 동기화된 영역에서 재정의할 수 있는 호출 x

      • 클라이언트에게 넘겨준 함수 객체 호출 x

    • 결과: 예외를 일으키거나, 교착 상태 야기

다음 코드에 ConcurrentModificationException 가 발생 이유는?

public static void main(String[] args) {
  ObservableSet<Integer> set =
    new ObservableSet<>(new HashSet<>());
  SetObserver<Integer> observer = new SetObserver<>() {
    @Override
    public void added(ObservableSet<Integer> set, Integer element) {
      System.out.println(element);
      if (element == 23)
        set.removeObserver(this);
    }
  };

  set.addObserver(observer);

  for (int i = 0; i < 100; i++)
    set.add(i);
}
// --------------------------------------------------
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); // Calls notifyElementAdded
    return result;
  }
}
// --------------------------------------------------
@FunctionalInterface 
public interface SetObserver<E> {
  // Invoked when an element is added to the observable set
  void added(ObservableSet<E> set, E element);
}
// --------------------------------------------------
class ForwardingSet<E> implements Set<E> {
  private final Set<E> s; // HashSet<String>
  public ForwardingSet(Set<E> s) { this.s = s; }

  public void clear()               { s.clear();            }
  public boolean contains(Object o) { return s.contains(o); }
  public boolean isEmpty()          { return s.isEmpty();   }
  public int size()                 { return s.size();      }
  public Iterator<E> iterator()     { return s.iterator();  }
  public boolean add(E e)           { return s.add(e);      } // add
  public boolean remove(Object o)   { return s.remove(o);   }
  public boolean containsAll(Collection<?> c)
  { return s.containsAll(c); }
  public boolean addAll(Collection<? extends E> c) // addAll
  { return s.addAll(c);      }
  public boolean removeAll(Collection<?> c)
  { return s.removeAll(c);   }
  public boolean retainAll(Collection<?> c)
  { return s.retainAll(c);   }
  public Object[] toArray()          { return s.toArray();  }
  public <T> T[] toArray(T[] a)      { return s.toArray(a); }
  @Override public boolean equals(Object o)
  { return s.equals(o);  }
  @Override public int hashCode()    { return s.hashCode(); }
  @Override public String toString() { return s.toString(); }
}

발생 이유

  • 리스트의 원소를 제거하려는데, 리스트를 순회하는 도중에 일어났기 때문

다음 코드는 왜 교착상태에 빠질까?

  • 예제 설명

    • 구독 해지를 직접 호출하지 않고, 다른 스레드한테 처리하도록 위임

발생 이유

  • 백그라운드 스레드가 removeObserver 를 호출하면 observers 를 잠그려고 시도하지만, 락을 얻을 수 없다. 메인 스레드가 이미 락을 쥐고 있기 때문이다. 그와 동시에, 메인스레드는 또 백그라운드 스레드가 관찰자를 제거하기만을 기다리는 중이다.

해결 방법

  • 외계인 메서드(=동기화 영역 바깥에서 온 메서드)를 동기화 블록 밖으로 뺀다

  • 더 나은 방법

    • CopyOnWriteArrayListarrow-up-right

      • "외계인 메서드를 동기화 블록 밖으로 뺀다" 목적 달성을 위해 ArrayList를 구현하여 설계

      • 내부를 변경하는 작업이 있으면, 복사본 생성

      • 다른 말로는, 느릴 수도....

        • 언제 사용?

          • 수정할 일은 드물고 순회가 빈번히 일어날때 (즉, 위 예제)

기본 규칙

  • Mutable 클래스 작성하고 싶을때

    • 동기화 하지 말기

      • 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자

      • doc에 "스레드에 안전하지 않음" 명시하기

      • 예, java.util

    • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자

      • 예, java.util.concurrent

  • StringBuffer가 있음에도 StringBuilder 가 나온 이유?

  • 비슷한 이유로 java.util.Random에서 추가적으로 java.util.concurrent.ThreadLocalRandom 생겨남

핵심 정리

  • 교착상태데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드 호출 금지

  • 동기화 영역 안의 작업은 최소화

  • 가변 클래스를 설계할 때는 스스로 동기화 할지 고민하기

  • 성능 측면에서, 동기화는 병렬로 실행할 기회를 잃게 할 수 있다.

  • 멀티코어 세상인 지금은 과도한 동기화를 피하는 게 과거 어느 때보다 중요

  • 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부를 문서에 명화히 밝히자 (아이템 82)

  • 공부하기

Last updated