NMP-Study / EffectiveJava2022

Effective Java Study 2022
5 stars 0 forks source link

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

Closed okhee closed 1 year ago

mbyul commented 1 year ago

78 에서 충분하지 못한 동기화의 피해를 다뤘다면, 이번엔 반대 상황

과도한 동기화는

동기화 메서드나 동기화 블록 안에서는 클라이언트에게 제어권을 넘기지 말 것

잘못된 코드(예외 발생) - 동기화 블록 안에서 외계인 메서드 호출

public class ObservableSet<E> extends ForwardingSet<E> {
   ...
    private final List<SetObserver<E>> observers = new ArrayList<SetObserver<E>> ();
   ...
    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;
    }
}

public interface SetObserver<E> { // 요소가 Set에 추가될 때 호출된다(call back)
   void added(ObservableSet<E> set, E element);
}
public static void main(String[] args) {
    ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
    set.addObserver(new SetObserver<>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if(e == 23) {
                s.removeObserver(this);
            }
        }
    });

    for(int i = 0; i < 100; i++) {
        set.add(i);
    }
}

잘못된 코드(교착 상태) - 쓸데없이 백그라운드 스레드를 사용하는 관찰자


public static void main(String[] args) {
    ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());

    // 실행 (executor) 서비스를 사용하는 observer(관찰자)
    set.addObserver(new SetObserver<>() {
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if(e == 23) {
                ExecutorService exec = Executors.newSingleThreadExecutor();
                try {

                    // 여기서 lock 발생 (메인 스레드는 작업을 기다리고 있음)
                    // 특정 태스크가 완료되기를 기다림 (submit().get()) (#80 참고)
                    exec.submit(() -> s.removeObserver(this)).get();

                } catch(ExecutionException | InterruptedException ex) {
                    throw new AssertionError(ex);
                } finally {
                    exec.shutdown();
                }
            }
        }
    });

    for (int i = 0; i < 100; i++) {
        set.add(i);
    }
}

예외 및 교착상태 해결법 - 1

// 동기화된 블록 밖으로 외계인 메소드를 옮긴다
private void notifyElementAdded(E element) {
         List<SetObserver<E>> snapshot = null; 
         synchronized(observers) {
             snapshot =new ArrayList<SetObserver<E>> (observers);
         }

         for (SetObserver<E> observer : snapshot)
                 observer.added(this, element) ;
}

예외 및 교착상태 해결법 - 2

성능 측면

그럼 어떻게?

사례

  1. StringBuffer는 거의 단일 스레드에서 쓰였음에도 내부적으로 동기화를 수행
    • 성능상의 이수로 뒤늦게 StringBuilder가 등장한 이유이기도 함
  2. java.util.Random은 동기화하지 않은 버전인 java.util.concurrent.ThreadLocalRandom으로 대체

그래서 뭘 쓰라고??

정리