Effective Java ‐ Item 79⚠️ - dnwls16071/Backend_Summary GitHub Wiki

아이템 79 - 과도한 동기화는 피하라.

응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에게 양도하면 안된다.

동기화(Synchronize)

  • 여러 쓰레드가 한 개의 리소스를 사용하려고 할 때, 사용하려는 쓰레드를 제외한 나머지들이 접근하지 못하게 막는 것 → Thread Safe
  • 동기화 방법
    • 메서드 자체 synchronized로 선언하는 방법
    • 블록으로 객체를 받아 lock을 거는 방법 - synchronized(this)
  • 멀티 쓰레드 프로세스는 동시성 또는 병렬성으로 실행된다.
    • 동시성 : 하나의 코어에서 여러 개의 프로세스가 번갈아 가면서 실행된다.
    • 병렬성 : 멀티 코어에서 개별 쓰레드를 동시에 실행한다.
  • 동기화(synchronized)된 코드 블럭 안에서는 재정의 가능한 메서드를 호출해선 안된다.
  • 클라이언트가 넘겨준 함수 객체를 호출해서도 안 된다.
  • 이런 메서드는 동기화된 클래스 관점에서 외계인 메서드라고 한다.(무슨 일을 할지 모르니, 이 메서드가 예외를 발생시키거나 교착상태를 만들거나 데이터를 훼손시킬 수 있다.)
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);
        }
    }
    
    // 외계인 메서드
    private void notifyElementAdded(E element){
        synchronized (observers){
            for(SetObserver observer: observers){
                observer.added(this,element);
            }
        }
    }

    public boolean removeObserver(SetObserver<E> observer){
        synchronized (observers){
            return observers.remove(observer);
        }
    }

    @Override
    public boolean add(E element) {
        boolean added = super.add(element);
        if(added)notifyElementAdded(element);
        return added;
    }

    public static void main(String[] args) {
        ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
	
        // 23 일 때 , 지우는 조건 추가
        // 재정의가 가능한 메서드 호출을 금지
        set.addObserver(new SetObserver<Integer>() {
            @Override
            public void added(ObservableSet<Integer> set, Integer element) {
                System.out.println(element);
                if (element == 23)
                    set.removeObserver(this);
            }
        });
        for (int i = 0; i < 100; i++) {
            set.add(i);
        }
    }

}

락의 재진입

  • 자바의 고유 락은 재진입이 가능하다. 재진입 가능하다는 것은 락 획득이 호출 단위가 아니라 쓰레드 단위로 일어난다는 것을 의미한다.
  • 이미 락을 획득한 쓰레드는 같은 락을 얻기 위해 대기할 필요가 없다.
public class Reentrancy {
    public synchronized void a() {
        System.out.println("a");
        // b가 synchronized로 선언되어 있지만 a진입시 이미 락을 획득하였으므로,
        // b를 호출할 수 있다.
        b();
    }
  
    public synchronized void b() {
        System.out.println("b");
    }
  
    public static void main(String[] args) {
        new Reentrancy().a();
    }
}

동기화의 성능

  • 자바의 동기화 비용은 빠르게 낮아져왔으나, 과도한 동기화를 피하는 일 역시도 중요하다.
  • 멀티코어가 일반화된 오늘날 과도한 동기화가 초래하는 진짜 비용은 락을 획득하는데 드는 CPU 시간이 아니다.
  • 쓰레드 간 경쟁하는 Race Condition에 낭비가 발생하는 것이다.
    • 병렬로 실행할 기회를 잃게 된다.
    • 모든 코어가 메모리를 일관되게 바라보기 위한 지연 시간이 진짜 비용
    • 가상 머신의 코드 최적화를 제한하는 점도 숨은 비용

결론

  • 기본 규칙은 동기화 영역에서 가능한 한 일을 적게 하는 것을 우선으로
  • 오래 걸리는 작업의 경우라면 동기화 영역 밖으로 옮기자.
  • 여러 쓰레드가 호출할 가능성이 있는 메서드가 정적 필드를 수정한다면 그 필드를 사용하기 전에 반드시 동기화해야 한다.
  • 교착 상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자.
  • 동기화 영역 안에서의 작업은 최소화하라.
⚠️ **GitHub.com Fallback** ⚠️