본문 바로가기
Language/Effective java

동시성

by y.j 2022. 8. 24.
728x90

과도한 동기화는 피하라

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

 


ObservableSet클래스

import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;

public class ObservableSet<E> extends ForwardingSet<E> {

    public ObservableSet(Set<E> s) {
        super(s);
    }

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

 

예제 1

삭제하는 순간에 객체의 List(observers)를 순회하고 있기 때문에 에러를 발생시킨다(ConcurrentModificationException).

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

    set.addObserver(new SetObserver<>() {
        @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);
}

 

예제 2

메인스레드가 이미 락을 쥐고 있는 상태이므로 백그라운드 스레드는 removeObserver를 수행하기 위하 락을 잡을 수 없다. 동시에 메인스레드는 객체를 제거하기만을 기다리고 있어 데드락 상태가 된다. 

set.addObserver(new SetObserver<>() {
    @Override
    public void added(ObservableSet<Integer> set, Integer element) {
        System.out.println(element);
        if(element == 23) {
            ExecutorService exec =
                    Executors.newSingleThreadExecutor();
            try {
                exec.submit(() -> set.removeObserver(this)).get();
            } catch (ExecutionException | InterruptedException ex) {
                throw new AssertionError(ex);
            } finally {
                exec.shutdown();
            }
        }
    }
});

 

해결방법 1

자바 언어의 락은 재진입을 허용하므로 교착상태에 빠지지는 않는다. 하지만, 안전실패가 발생 할 수 있으므로 객체를 복사해서 쓴다면 해결 할 수 있다.

private void notifyElementAdded(E element) {
    synchronized (observers) {
        snapshot = new ArrayList<>(observers);
    }
    for (SetObserver<E> observer : snapshot) {
        observer.added(this, element);
    }
}

 

해결방법 2

CopyOnWriteArrayList가 정확히 이목적으로 특별히 설계된 것이다. 항상 깨끗한 복사본을 만들어 락이 필요없어 매우 빠르다. 

*하지만 다른 용도로 쓰이면 끔찍하게 느리다.

private final List<SetObserver<E>> observers = new CopyOnWriteArrayList<>();

public void addObserver(SetObserver<E> observer) {
    synchronized (observers) {
        observers.add(observer);
    }
}

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

 

기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다(ITEM78).

 

가변 클래스를 작성하려거든 다음 두 선택지 중 하나를 따르자.

  • 동기화를 전혀 하지 말고 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자.
  • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자(ITEM 82).

 

 

 

728x90

'Language > Effective java' 카테고리의 다른 글

동시성  (0) 2022.09.02
동시성  (0) 2022.08.27
동시성  (0) 2022.08.22
예외  (0) 2022.08.19
예외  (0) 2022.08.18

댓글