본문 바로가기

책정리/EfectiveJava

과도한 동기화는 피하라

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

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

  • 동기화된 영역 안에서는 재정의할수 있는 메서드를 호출하면 안됨
  • 클라이언트가 넘겨준 함수객체를 호출해서도 안됨
  • 동기화된 클래스 관점에서는 이런 메서드는 모두 외계인 메서드다
  • 외계인 메서드는 하는일에 따라 동기화된 영역은 예외를 일으키거나 교착상태에 빠지거나 데이터를 훼손할 수 있다.

 

외계인 메서드 호출


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;
    }

관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청 또는 해지 함

두 경우 다음 콜백 인터페이스의 인스턴스를 메서드에 전달

 

public interface SetObserver<E> {    
    void added(ObservableSet<E> set, E element);	
}

 구조적으로 BiConsumer<ObservableSet<E>, E>와 같다.

 커스텀 함수형 인터페이스를 정의한 이유는 이름이 더 직관적이고 다중 콜백을 지원하도록 확장할 수 있기 때문이다.

 

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);
}

위 코드는 잘 동작 한다. 

 

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

        set.addObserver(new SetObserver() {
	       @Override
	       public void added(ObservableSet s, Object e) {
		       System.out.println(e);

		       if ((int) e == 23) {
		       	    s.removeObserver(this);
		       }
	        }
           }
        );

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

위 코드는 0부터 23까지 출력한 다음 자신을 구독 해지한다.

실제로 23까지 출력한 다음 ConcurrentModificationException을 던진다.

관찰자의 added메서드 호출이 일어난 시점이 notifyElementAdded가 관찰자들의 리스트를 순회하는 도중이기 때문

added 메서드는 ObservableSetdml remove메서드를 호출 하고 이 메서드는 다시 observers.remove를 호출 함

 

여기서 원소를 제거하면 ArrayLIst에서는 modCount가 증가 하게 되고 notifyElementAdded순회 할때 checkForComodification

을 통해 modCount 와 expectedModCount를 비교하게 되는데 modCount가 1이였지만 순회하는 도중 remove로 인해 2로 변경 되어 

ConcurrentModificationException을 던지게 된다.

 

notifyElementAdded메서드에서 수행하는 순회는 동기화 블록 안에 있으므로 동시 수정이 일어나지 않지만, 자신이 콜백을 거쳐 되돌아와 수정 하는 것까지 막지 못한다.

 

 ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
        
        set.addObserver(new SetObserver() {
            @Override
            public void added(ObservableSet s, Object e) {
                System.out.println(e);
                if ((int) e == 23) {
                    ExecutorService exec =
                            Executors.newSingleThreadExecutor();
                    try {
                        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);
    }

removeObserver를 직접 호출하지 않고 ExcutorService를 사용해 다른 스레드에게 부탁

위 코드는 교착상태에 빠지게 된다.

백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 시도하지만 락을 얻을 수 없다.

메인 스레드가 이미 락을 쥐고 있기 때문

메인 스레드는 백그라운드 스레드가 관찰자를 제거하기만 기다리고 있다.

 

 

교착상태 및 예외 해결 방법


자바는 재진입(reentrant)을 허용함으로 교착상태에 빠지지 않는다.

재진입 가능 락은 객체 지향 멀티스레드 프로그램을 쉽게 구현하게 해주지만 교착상태나 데이터 훼손으로 변모시킬 수 있다.

이런 문제를 해결하는 방법은 외계인 메서드 호출을 동기화 블록 밖으로 옮기면 된다.

아래 코드처럼 동기화 영역 밖에서 호출되는 외계인 메서드를 열린 호출(open call)이라고 한다.

 

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);
}

 

더 나은 방법은 CopyOnWriteArrayList를 사용하는 것이다.

CopyOnWriteArrayList는 ArrayList를 구현한 클래스로 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행한다.

수정할 일은 드물고 순회만 빈번히 일어나는 관찰자 리스트 용도로는 최적이다.

 

 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);
    }

동기화 성능

 

자바의 동기화 비용은 빠르게 낮아져 왔지만, 과도한 동기화를 피하는일은 오히려 과거 어느 때보다 중요하다.
멀티코어가 일반화된 오늘날 과도한 동기화가 초래하는 진짜 비용은 락을 얻는데 드는 CPU 시간이 아니다.
서로 스레드끼리 경쟁하는 Race Condition에 낭비가 발생한다.

  • 병렬로 실행할 기회를 잃는다.
  • 모든 코어가 메모리를 일관되게 보기위한 지연시간이 진짜 비용
  • 가상머신의 코드최적화를 제한하는 점도 숨은 비용

가변 클래스를 작성하는 경우 동기화에 대해 고려할 점

  1. 동기화를 전혀 하지 말고 가변 클래스를 동시에 사용해야하는 클래스가 외부에서 동기화하자
  2. 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자.
    (단 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 두번째 방법을 쓴다)

 

 

정리

 

기본규칙은 동기화 영역에서는 가능 한 일을 적게 하는것 (락을 얻고 공유 데이터를 검사하고, 필요하면 수정하고, 락을 놓는다.)

오래 걸리는 작업이라면 아이템 78지침을 지키면서 동기화 바깥으로 옮기는 방법을 찾자.

교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자

가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자

 

 

 

 

https://m.blog.naver.com/PostView.nhn?blogId=tmondev&logNo=220393974518&proxyReferer=https%3A%2F%2Fwww.google.com%2F

 

https://jaehun2841.github.io/2019/03/12/effective-java-item79/#%EC%99%B8%EA%B3%84%EC%9D%B8-%EB%A9%94%EC%84%9C%EB%93%9C

 

https://www.charlezz.com/?p=768

 

안드로이드로 배우는 자바의 인터페이스 (콜백, 리스너, 옵저버) | 찰스의 안드로이드

Before diving into interface 초보 자바 개발자 분들의 단골로 하는 질문이 있습니다. “리스너(콜백)가 뭐에요?” 또는 “인터페이스가 뭔지 모르겠어요!!” 자바의 인터페이스에 대한 개념없이 리스너를 이해하려 하는것은 헤어나올 수 없는 늪에 빠진것과 같습니다. 초보자 분들을 위해 인터페이스의 개념도 알아보고 실제 용례도 알아보고자 이 포스팅을 작성합니다. What is Interface? 포스팅을 작성하기 위해 인터페이스에 대한 정의부

www.charlezz.com