public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver((s, e) -> System.out.println(e));
for (int i = 4; i <= 100; i++) {
set.add(i);
}
}
4~100의 숫자를 출력하고 종료된다.
문제가 없어 보이지만, 외계인 메서드를 observer에 등록시켜 동기화된 블록에서 사용하고 있다.
1️⃣ 자기 자신을 제거하는 관찰자 : 동시성 예외
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if (element == 23)
set.removeObserver(this);
}
});
람다를 사용하면 자신을 참조할 수단이 없어서 익명 클래스로 대체했다.
23에서 조용히 종료되지 않고 ConcurrentModificationException을 던진다.
synchronized는 동시성 보장을 해주지만, 정작 자신이 콜백을 거쳐 되돌아와 수정하는 것을 막지 못한다.
즉, Java의 Lock은 재진입(reentrant)를 허용한다.
자신을 제거하는 외계인 메서드 added가 synchronized 블럭 내에서 실행되면서 순회 도중인 리스트의 원소를 제거하려 시도한다.
전에 공부할 때 왜 이렇게 구현해놨을까 싶었는데, OS를 공부하다가 유레카를 외치며 다시 뛰어왔다.
Java의 synchronized 메서드 혹은 블럭에 의해 동기화 되는 객체들은 각자 고유한 monitor가 결합이 되어 Synchronization 작업을 수행한다. 그런데 인스턴스 별로 결합된 Monitor가 Lock count 정보를 유지하기 때문에 동일한 Thread에서 lock을 걸 수 있게 되어 버린다. (어차피 자기 자신이 관리하고 있으므로)
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); // 콜백 메서드
}
}
내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현됐다.
내부 배열은 불변하므로 Lock이 필요없어서 매우 빠르다.
단, 수정할 일은 드물고 순회만 빈번이 일어나는 관찰자 리스트 용도로 최적이다.
다른 용도로 쓰인다면 매번 복사해야 하므로 끔찍이 느릴 수도 있다.
📌 기본 규칙
💡동기화 영역에서는 가능한 한 일을 적게 하라
Lock을 얻고, 공유 데이터를 검사하고, 필요하면 수정하고, Lock을 놓는다.
오래 걸리는 작업이라면, Item 78의 지침을 지키면서 Open call을 사용하는 방법을 찾아보라
📌 지연 시간(Latency)
요즘같이 멀티 코어가 일반화된 오늘날, 과도한 동기화가 초래하는 진짜 비용은 Lock을 얻는 데 드는 CPU 시간이 아니다.
진짜 문제는 경쟁하느라 낭비하는 시간이다.
병렬로 실행할 기회를 잃는다.
모든 코어가 memory을 일관되게 보기 위한 지연시간이 발생한다.
JVM의 코드 최적화를 제하한다는 점도 과도한 동기화의 숨은 문제점이다.
가변 클래스를 작성하거든, 아래 두 가지 선택지 중 하나를 따라라
1️⃣ 동기화를 전혀 하지 말고, 해당 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하라
public class Foo {
private int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class ExternalClass {
public static void main(String[] args) throws InterruptedException {
Foo foo = new Foo(0);
// 하나의 스레드에서 값 설정하는 작업
Runnable setTask = () -> {
for (int i = 0; i < 1000; i++) {
foo.setValue(i);
}
};
// 다른 스레드에서 값 읽는 작업
Runnable getTask = () -> {
for (int i = 0; i < 1000; i++) {
int value = foo.getValue();
if (value != i) {
System.out.println("Inconsistent value detected: " + value);
}
}
};
Thread thread1 = new Thread(setTask);
Thread thread2 = new Thread(getTask);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Done");
}
}
동기화 되지 않은 가변 객체 Foo를 사용하기 위해서, Client가 동기화하도록 구현했다.