Item 79 : 과도한 동기화는 피하라
2021. 9. 16. 19:41
- 이전 장 아이템 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를 구현한 클래스로 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현되었다.
- 순회용으로 최적이다.
- 자바 동시성 컬렉션 라이브러리의 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);
}
- 아까 위에서 처럼 메소드를 동기화 블록 바깥으로 뺀 메소드를 열린 호출(open call)이라 한다.
동기화에서 가장 중요한 것은 낭비되는 시간, 즉 병렬로 실행할 기회를 읽고 모든 코어가 메모리를 일관되게 보기 위한 지연시간이 진짜 비용이다.
- 가변 클래스를 작성할 때 두가지를 명심하자.
- 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자
- 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자.
- 단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두번째 방법을 선택해야 한다.
'책 > 이펙티브자바' 카테고리의 다른 글
Item 81 : wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2021.09.19 |
---|---|
Item 80 : 스레드보다는 실행자, 태스크, 스트림을 애용하라. (0) | 2021.09.17 |
Item 78 : 공유 중인 가변 데이터는 동기화해 사용하라 (0) | 2021.09.15 |
Item 77 : 예외를 무시하지 말라 (0) | 2021.09.13 |
Item 76 : 가능한 한 실패 원자적으로 만들라 (0) | 2021.09.12 |