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 를 잠그려고 시도하지만, 락을 얻을 수 없다. 메인 스레드가 이미 락을 쥐고 있기 때문이다. 그와 동시에, 메인스레드는 또 백그라운드 스레드가 관찰자를 제거하기만을 기다리는 중이다.
해결 방법
외계인 메서드(=동기화 영역 바깥에서 온 메서드)를 동기화 블록 밖으로 뺀다
더 나은 방법
"외계인 메서드를 동기화 블록 밖으로 뺀다" 목적 달성을 위해 ArrayList를 구현하여 설계
내부를 변경하는 작업이 있으면, 복사본 생성
다른 말로는, 느릴 수도....
언제 사용?
수정할 일은 드물고 순회가 빈번히 일어날때 (즉, 위 예제)
기본 규칙
Mutable 클래스 작성하고 싶을때
동기화 하지 말기
그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자
doc에 "스레드에 안전하지 않음" 명시하기
예, java.util
동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자
예, java.util.concurrent
StringBuffer가 있음에도 StringBuilder 가 나온 이유?
StringBuffers: 멀티쓰레드 환경에서 사용
싱글 스레드에서 사용시 동기화로 성능 저하
StringBuilder: 싱글쓰레드 환경에서 사용
비슷한 이유로 java.util.Random에서 추가적으로 java.util.concurrent.ThreadLocalRandom 생겨남
핵심 정리
교착상태와데이터 훼손을 피하려면 동기화 영역 안에서외계인 메서드호출 금지동기화 영역 안의
작업은 최소화가변 클래스를 설계할 때는 스스로 동기화 할지 고민하기성능 측면에서, 동기화는 병렬로 실행할 기회를 잃게 할 수 있다.
멀티코어세상인 지금은 과도한 동기화를 피하는 게 과거 어느 때보다 중요합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부를 문서에 명화히 밝히자 (아이템 82)
공부하기
클래스 내부 동기화
클래스 내부에서 동기화하려면, 락 분할(lock splitting), 락 스트라이핑(lock stripping), 비차단 동시성 제어(nonblocking concurrency control) 등 기법을
StringBuffer가 있음에도 StringBuilder 가 나온 이유?
java.util.Random -> java.util.concurrent.ThreadLocalRandom
Last updated