요약
•
동기화 영역 내에서 외계인 메서드를 호출하면 에러나 교착 상태가 발생할 수 있고 데이터가 훼손될 수 있으니, 가급적 동기화 영역 밖에서 외계인 메서드를 호출(열린 호출) 하자.
•
동기화를 과하게 하면, 많은 비용이 발생하고 성능이 떨어지니 주의하자.
동기화 영역에서 외계인 메서드 호출 금지
•
응답 불가와 안전 실패를 피하려면 동기화 메서드나동기화 블록 안에서는 제어를 클라이언트에 양도하면 안된다.
•
동기화된 영역 안에서 재정의할 수 있는 메서드를 호출해서는 안되고, 클라이언트가 넘겨준 함수 객체(외계인 메서드)도 호출하면 안된다.
•
그렇지 않는다면 동기화된 영역에서 에러를 일으키거나, 교착상태에 빠지거나, 데이터 훼손할 수 있다.
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;
}
}
@FunctionalInterface
public interface SetObserver<E> {
// ObservableSet에 원소가 더해지면 호출된다.
void added(ObservaleSet<E> set, E element);
}
Java
복사
•
위 코드에서 added라는 추상 메서드를 외부에서 정의하여 동작시켜, 정상 동작을 하고 에러를 일으키고 교착상태에 빠지는 예시를 살펴볼 것이다.
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);
}
Java
복사
•
위 코드는 동작의 예시를 보여주기 위한 아주 간단한 코드로, add()를 호출할 때마다 외계인 메서드로 제공한 람다식을 동작하여 0부터 99까지 출력할 것이다.
set.addObserver(new SetObserver<>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23)
s.removeObserver(this);
}
})
Java
복사
•
만약 외계인 메서드를 위의 익명 클래스에서 정의한 메서드로 대체해 실행시킨다고 하면, 0부터 99까지를 요소로 add()를 호출하고 요소가 23일 때 자기 자신(익명 클래스)을 지우려할 것이다.
•
add(23)을 호출 받아 notify를 위해 observers를 순회하며 observer의 메서드를 호출한 상황에 위 코드가 실행되는 것이라서, 해당 메서드가 포함된 자기자신 observer를 삭제하려하면 concurrentModificationException이 발생한다.
set.addObserver(new SetObserver<>() {
public void added(ObservableSet<Integer> s, Integer e) {
System.out.println(e);
if (e == 23) {
ExecutorService exec =
Executors.newSingleThreadExcutor();
try {
exec.submit(() -> s.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException ex) {
throw new AssertionError(ex);
} finally {
exec.shutdown();
}
}
}
}
Java
복사
•
위 코드는 이전의 에러가 발생하는 코드와 기능은 동일하지만, 외계인 메서드를 단일 스레드에서 실행시키는 것이 아닌 새로운 스레드를 만들어 실행시킨다.
•
이 코드에서의 문제는 notifyElementAdded 메서드와 removeObserver 메서드 모두 observers 객체로 synchronized 되어 있기 때문에, 서로 lock을 필요로 하는 상황에 한 쪽이 잡고 있으므로 교착상태에 빠지게 된다.
자바 기능…?
•
위의 문제가 되는 2가지 예제처럼 클라이언트가 제공하는 외계인 메서드는 얼마나 오래 실행되고 어떤 동작을 수행할 지 모르니, 동기화 영역 안에서는 외계인 메서드를 호출하지 말자.
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);
}
Java
복사
•
위의 에러가 발생하거나 교착 상태가 발생하는 코드는 이와 같이 내부 배열을 복사하여 외계인 메서드 호출을 동기화 영역 밖으로 옮기면 해결된다.
private final List<SetObserver<E>> observers =
new CopyOnWriteArrayList<>();
Java
복사
•
자바에 이런 기능을 미리 구현해둔 라이브러리가 있다. 위 코드에서의 CopyOnWriteArrayList는 이름 그대로 내부를 변경할 때 리스트를 복사하여 작업을 수행한다.
•
이와 같이 동기화 영역 밖에서 외계인 메서드를 호출하는 것을 열린 호출(open call)이라 한다. 열린 호출을 사용하면 실패 방지 효과와 동시성 효율을 크게 개선해준다.
과도한 동기화 금지
•
과도한 동기화를 하게되면 경쟁하느라 시간 낭비를 하게되고, 병렬로 실행할 기회를 잃고, 모든 코어가 메모리를 일관되게 보기 위한 시간 지연이 발생하게 된다.
•
만약 가변 클래스를 작성한다면, 동기화를 전혀하지말고 해당 클래스를 동시에 사용해야 하는 외부에서 알아서 동기화하게 만들자.
•
만약 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선 할 수 있다면, 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자.
•
클래스 내부에서 동기화한다면, 락 분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 통해 동시성을 높일 수 있다.
◦
락 분할
락을 독립적인 상태 변수마다 따로 분리하여, 락의 정밀도를 높이는 방식
◦
락 스트라이핑
자료 구조나 구간 블록을 여러 개의 락으로 분할하여, 특정 구간마다 담당하는 락을 지정해 락의 정밀도를 높이는 방식
◦
비차단 동시성 제어
non-blocking으로 여러 접근을 제어하는 I/O Multiplexing이나 낙관적 락, CAS처럼 lock 없이 경쟁 상태를 최소화하면서 동시성을 제어하는 방식