Java/EffectiveJava

Effective Java Item79. 과도한 동기화는 피하라.

Flambee 2025. 3. 16. 00:08

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

응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다. 동기화된 영역을 포함한 클래스 관점에서는 재정의 함수나, 함수 객체 모두 포함된다.

이 경우 메서드 호출을 동기화 블록 바깥으로 옮기면 된다.

  • CopyOnWriteArrayList 사용 : ArrayList를 구현한 클래스로, 내부를 변경하는 자업은 항상 깨끗한 복사본을 만들어 수행하도록 구현했다.
  • 내부의 배열은 절대 수정되지 않으니 순회할 때 락이 필요 없어 매우 빠르다.
  • 수정할 일은 드믈고 순회만 빈번히 일어나는 관찰자 리스트 용도로 최적이다.

열린 호출(OpenCall)

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.util
  • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자. 단 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두 번째 방법을 사용하자.
    • java.util.concurrent
    • 락 분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 동원해 동시성을 높여줄 수 있다.
  • 여러 스레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면 그 필드를 사용하기 전에 반드시 동기화해야한다.
    • 결과적으로 이 정적 필드가 심지어 private라도 서로 관련 없는 스레드들이 동시에 읽고 수정할 수 있게 된다.

결론

  • 동기화 영역 안에서의 작업은 최소한으로 줄이자.
  • 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자.
  • 멀티코어 세상인 지금은 과도한 동기화를 피하는 게 과거 어느 때보다 중요하다.
  • 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자.