반응형
이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
아이템 78에서는 동기화의 필요성을 다뤘다면, 이번에는 동기화의 남용에 대해 다룬다.
동기화 유의점
- 과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠트리며 예측할 수 없는 동작을 야기할 수도 있다.
- 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
- 안전실패 : 프로그램이 잘못된 결과를 계산하는 것
- 예로, 동기화된 영역 안에서는 재정의할 수 있는 메서드는 호출하면 안되며, 클라이언트가 넘겨준 함수 객체를 호출해서는 안된다.
- 동기화된 영역을 포함한 클래스 관점에서는 이런 메서드는 모두 바깥 세상에서 온 외계인이며, 그 메서드가 무슨 일을 하는 지 모르고 통제할 수도 없다. (외계인 메서드)
- 이러한 외계인 메서드에 따라 동기화된 영역은 예외, 교착상태를 일으키거나 데이터를 훼손할 수도 있다.
ex 1) 동기화 블록 안에서 외계인 메서드를 호출하는 잘못된 예
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;
}
}
- 어떤 집합(Set)을 감싼 래퍼 클래스인데, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있는 예제이다.
- 관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지한다.
- 두 경우 모두 다음 콜백 인터페이스의 인스턴스를 메시지에 건넨다.
ex 2) 커스텀 함수형 인터페이스
@FunctionalInterface public interface SetObserver<E> {
// ObservableSet에 원소가 더해지면 호출된다.
void added(ObservableSet<E> set, E element);
}
- 위의 커스텀 함수형 인터페이스를 정의한 이유는 이름이 더 직관적이고 다중 콜백을 지원하도록 확장할 수 있기 때문이다.
ex 3) observableSet을 사용한 main 메서드 예
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((set1, element) -> System.out.println(element));
for (int i = 0; i < 100; i++)
set.add(i);
}
- 위의 예제는 0부터 99까지 출력한다.
ex 4) 다른 익명함수를 넘기는 예
public static void main(String[] args) {
ObservableSet<Integer> set =
new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<>() {
@Override
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) // 값이 23이면 자기 자신을 제거
s.removeObserver(this);
}
});
for (int i = 0; i < 100; i++)
set.add(i);
}
- 위의 예에 대한 기대는 0부터 23까지 출력한 후 관찰자 자신을 구독해지한 다음 조용히 종료할 것이다.
- 하지만, 실제로는 23까지 출력한 다음 ConcurrentModificationException을 던진다.
- 관찰자의 added 메서드 호출이 일어난 시점이 notifyElementAdded가 관찰자들의 리스트를 순회하는 도중이기 때문이다.
- added 메서드는 ObserableSet의 remove Observer 메서드를 호출하고, 이 메서드는 다시 observers.remove메서드를 호출하는데, 리스트에서 원소를 제거할 때 이 리스트는 이미 순회하는데 사용되는 것이다.
- 즉, notifyElementAdded 메서드에서 수행하는 순회는 동기화 블록 안에 있으므로 동시 수정이 일어나지 않도록 보장하지만, 정작 자신이 콜백을 거쳐 되돌아와 수정하는 것까지는 막지 못한다.
ex 5) 교착상태를 일으키는 예
set.addObserver(new SetObserver<Integer>() {
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();
}
}
}
});
- 이 예는 예외는 발생하지 않지만, 교착상태에 빠진다.
- 백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 시도하지만 락을 얻을 수 없다.
- 메인 스레드가 이미 락의 주체이기 때문이다.
- 그와 동시에 메인 스레드는 백그라운드 스레드가 관찰자를 제거하기를 기다린다. 즉, 서로 기다린다.
- 실제 시스템에서도 이렇게 동기화된 영역 안에서 외계인 메서드를 호출하여 교착상태에 빠지는 사례가 자주 있다.
- 해결 방법
- 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 해결할 수 있다.
- java.util.concurrent.CopyOnWriteArrayList 사용
동기화 팁
- 기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.
- 가변 클래스를 작성하는 경우 아래 두 가지 중 하나를 따르자.
- 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 동기화 하도록 한다.
- 동기화를 내부에서 수행해 스레드 안전한 클래스로 만든다. (단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때)
정리
- 교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자.
- 동기화 영역 안에서의 작업은 최소한으로 줄이자.
- 가변 클래스를 설계할 때는 스스로 동기화해야 할 지 고민하자.
- 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부를 문서에 밝히자.
반응형
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2022.12.22 |
---|---|
[Effective Java] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 이용하라 (0) | 2022.12.21 |
[Effective Java] 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라 (0) | 2022.12.19 |
[Effective Java] 아이템 77. 예외를 무시하지 말라 (0) | 2022.12.16 |
[Effective Java] 아이템 76. 가능한 한 실패 원자적으로 만들라 (0) | 2022.12.16 |