티스토리 뷰

동기화 블럭에서 제어를 클라이언트에 양도한 예시와 문제점

  • 동기화된 영역 안 에서는 재정의할 수 있는 메서드는 호출하면 안 되며,클라이언트가 넘겨준 함수 객체(아이템 24)를 호출해서도 안 된다.
  • 동기화된 영역을 포함한 클래스 관 점에서는 이런 메서드는 모두 바깥 세상에서 온 외계인이다.
  • 그 메서드가 무슨 일을 할지 알지 못하며 통제할 수도 없다는 뜻이다. 외계인 메서드(alien method)가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착상태에 빠지거나, 데이터를 훼손할 수도 있다.
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) { // Set에 add하면 관찰자의 added 메서드를 호출한다.
        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;
    }
}
@FunctionaUnterface public interface SetObserver<E> { 
    // ObservableSet에 원소가 더해지면 호출된다.
    void added(ObservableSet<E> set, E element);
}
public static void main(String[] args) { 
    ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>(》);
    set.addObserver((s, e) -> System.out.printIn(e));
    for (int i =0; i <100; i++) set.add(i);
}
set.addObserver{new SetObserver<> () {
    public void added{ObservableSet<Integer> s, Integer e) {
        System.out.println(e);
        if(e= 23) 
            s.removeObserver(this);
    }
)};

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

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

        // 코드 79-2 쓸데없이 백그라운드 스레드를 사용하는 관찰자 (423쪽)
        set.addObserver(new SetObserver<Integer>() {
            public void added(ObservableSet<Integer> s, Integer e) {
                System.out.println(e);
                if (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);
    }
}
set.addObserver(new SetObserver〇 () {
    public void added(ObservableSet<Integer> s, Integer e》 {
        System.out.println(e); 
        if(e= 23) {
            ExecutorServ丄ce exec = Executors.newSingleThreadExecutor();
            try {
                exec.submit(() -> s.removeObserver(this)).get();
            } catch {ExecutionException | InterruptedException ex) { 
                    throw new AssertionError(ex);
            } finally { 
                exec.shutdown();
            }
    }
});
  • 이 프로그램을 실행하면 예외는 나지 않지만 교착상태에 빠진다. 백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 시도하지만 락을 얻 을 수 없다. 메인 스레드가 이미 락을 쥐고 있기 때문이다. 그와 동시에 메인 스레드는 백그라운드 스레드가 관찰자를 제거하기만을 기다리는 중이다.

해결 방법

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);
}
  • 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 된다. notifyElementAdded 메서드에서라면
    관찰자 리스트 를 복사해 쓰면 락 없이도 안전하게 순회할 수 있다. 이 방식을
    적용하면 앞서의 두 예제에서 예외 발생과 교착상태 증상이 사라진다.
  • CopyOnWriteArrayList가 정확히 이 목적으로 특별히 설계된 것이다. 이름이 말해주듯 ArrayList를 구현한 클래스로 , 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현했다. 내부의 배열은 절대 수정되지 않으니 순회할 때 락이 필요 없어 매우 빠르다. 다른 용도로 쓰인다면 CopyOnWriteArrayList는 끔찍이 느리겠지만,수정할 일 은 드물고 순회만 빈번히 일어나는 관찰자 리스트 용도로는 최적이다

가변 클래스를 작성할 때 따라야할 선택지

  1. 동기화를 고려하지 말고 사용하는 측에서 고려하도록 하라
  2. 동기화를 내부에서 수행해 스레드안전하게 만들자(외부에서 전체에 락을 거는 것보다 더 효율이 높을 시에만)
  • 락 분할(lock splitting) - 하나의 클래스에서 기능적으로 락을 분리해서 사용하는 것(두개 이상의 락 - ex: 읽기전용 락, 쓰기전용 락)
  • 락 스트라이핑(lock striping) - 자료구조 관점에서 한 자료구조 전체가 아닌 일부분(buckets or stripes 단위)에 락을 적용하는 것
  • 비차단 동시성 제어(nonblocking concurrency control)

핵심 정리

교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자. 일반화해 이야기하면, 동기화 영역 안에서의 작업은 최소한으로 줄이자. 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자. 멀티코어 세상인 지금은 과도한 동기화를 피하는게 과거 어느 때보다 중요하다. 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화했는지 여부를 문서에 명확히 밝히자

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함