1. 과도한 동기화
과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리고, 심지어 예측할 수 없는 동작을 낳기도 한다.

1-1. 정확성을 떨어뜨린다.
synchronized 를 활용한 동기화 메서드, 동기화 블록을 아이템 78에서 확인했다. 이 안에서는 절대로 제어를 클라이언트에게 양도하면 안된다. 알지 못하고 검증되지 않은 메서드는 무슨 일을 할지 몰라 통제할 수 없기 때문이다. 예외를 일으키기도 하고, 데이터를 훼손시킬 수도 있다. 아래 집합을 감싼 ObservableSet 래퍼 클래스를 확인해보자.
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;
}
}
관찰자 패턴으로, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있다. 관찰자들은 addObserver 와 removeObserver 메서드를 호출해 구독을 신청, 해지할 수 있다. 그럼, ObserverSet 을 사용해 0부터 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<Integer>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) s.removeObserver(this);
}
});
그러면, 0부터 100까지 흘러가는데, 와중에 23을 만났을 때 구독을 해지하고 더 이상 출력하지 않을까? 실제로는 그렇게 진행되지 않는다. 23까지 출력 후에 ConcurrentModificationException 을 던진다. 왜 그럴까? added 메서드를 관찰자의 리스트를 순회하는 도중 만나기 때문이다. added 메서드는 removeObserver 를 호출하고, removeObserver 는 observers.remove 메서드를 호출하는데, 원소를 제거하려던 찰나 이 리스트를 순회하는 중인 것이다. 그러니까, 동기화된 메서드 안에서 외부의 메서드를 막 사용하지 말았어야 한다. 다른 예시도 있다.
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();
}
}
}
});
구독해지를 하는데, removeObserver 를 직접 호출하지 않고 다른 쓰레드에게 부탁하는 것이다. 이 때, 교착상태에 빠질 것이다. 백그라운드 쓰레드가 s.removeObserver 를 호출했을 때, 이미 메인쓰레드가 락을 쥐고 있고, 메인 스레드는 대기할 것이다. 지금까지는 불변식이 깨질 일은 없었는데, 그렇다면 똑같은 상황에서 임시로 불변식이 깨진 경우를 대비해서, 이렇게 해결할 수 있다.
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);
}
added 를 사용한 부분을 밖으로 synchronized 밖으로 옮기면 된다. 이를 열린호출 이라고 한다. 이 외에도 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게 순회할 수 있는데, 자바에서는 CopyOnWriteArrayList 를 제공한다. 아래처럼 동기화 한 곳이 없어진 형태이다.
private final List<SetObserser<E>> observers = new CopyOnWriteArrayList<>(); // CopyOnWriteArrayList로 생성
public void addObserver(SetObserver<E> observer) { // 관찰자 추가
observers.add(observer);
}
public boolean removeObserver(SetObserver<E> observer) { // 관찰자 제거
return observers.remove(observer);
}
public void notifyElementAdded(E element) { // 원소가 추가될 때 관찰자들에게 알림을 보내는 메소드
for (SetObserver<E> observer : observers) {
observers.added(this, element);
}
}
지금까지 한 약소한 결론은, 동기화 영역에서는 가능한 한 일을 적게 하자는 것이다.
1-2. 성능을 떨어뜨린다.
과도한 동기화로 인해, 정말 낭비하는 비용은 쓰레드 간 경쟁 비용이다. 그리고, JVM 의 코드 최적화 또한 제한한다.
2. 정리
가변 클래스를 작성할 때에는 두 가지 선택지 중 하나를 따르자.
1. 동기화는 하지 말고, 해당 클래스를 동시에 사용해야 하는 클래스가 알아서 동기화하게 하자.
2. 동기화를 내부에서 수행해 쓰레드 안전한 클래스로 만들자.
단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만.
2번 방식으로 클래스를 내부에서 동기화하기로 했다면, 락 분할, 락 스트라이핑, 비차단 동시성 제어 등 다양한 기법으로 동시성을 높일 수 있다. 그리고, 여러 스레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면, 그 필드를 사용하기 전에 반드시 동기화하자.
'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글
| [아이템81] wait 와 notify 보다는 동시성 유틸리티를 애용해라 (0) | 2024.12.18 |
|---|---|
| [아이템80] 스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) | 2024.12.16 |
| [아이템78] 공유 중인 가변 데이터는 동기화해 사용하라 (0) | 2024.12.11 |
| [아이템77] 예외를 무시하지 말라 (0) | 2024.12.11 |
| [아이템76] 가능한 한 실패 원자적으로 만들라 (0) | 2024.12.10 |