티스토리 뷰
728x90
반응형
과도한 동기화는 피하라
- 과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠질 수 있으며 예측할 수 없는 동작을 유발할 수 있습니다.
💡 응답불가와 안전실패를 피하려면 동기화 메서드나 블럭안에서는 제어권을 클라이언트에게 양도하면 안됩니다.
- 동기화된 영역안에서는 재정의할 수 있는 메서드를 호출해서는 안됩니다.
- 클라이언트가 넘겨준 함수(람다)를 호출해서는 안됩니다.
💡 예제 코드
@FunctionalInterface
public interface SetObserver<E> {
void added(ObservableSet<E> set, E element);
}
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) { this.s = s; }
@Override
public int size() { return s.size(); }
@Override
public boolean isEmpty() { return s.isEmpty(); }
@Override
public boolean contains(Object o) { return s.contains(o); }
@Override
public Iterator<E> iterator() { return s.iterator(); }
@Override
public Object[] toArray() { return s.toArray(); }
@Override
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override
public boolean add(E e) { return s.add(e); }
@Override
public boolean remove(Object o) { return s.remove(o); }
@Override
public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override
public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override
public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override
public void clear() { s.clear(); }
}
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);
}
}
public void notifyElementAdded(E element) {
synchronized (observers) {
for (SetObserver<E> observer : observers) {
observer.added(this, element);
}
}
}
@Override
public boolean add(E e) {
boolean added = super.add(e);
if (added) {
notifyElementAdded(e);
}
return added;
}
@Override
public boolean addAll(Collection<? extends E> c) {
boolean result = true;
for (E element : c) {
result |= add(element);
}
return result;
}
}
🧨 문제가 발생할 수 있는 상황 - 예외 발생
- 아래 코드는 element가 23일 때 구독을 해지하고 종료할거 같지만 실제로는 ConcurrentModificationException이 발생하게 됩니다. 이는 리스트에서 원소를 제거할려는데 동시에 리스트를 순회하려 하기 때문입니다.
- add 메서드 내부에서는 notifyelementAdded메서드가 수행되고 있고 main 메서드의 added 메서드에서는 removeObserver 메서드를 호출하고 있는데 여기서 문제가 발생하게 됩니다. 리스트에서는 원소 23인 경우 제거할려고하고 있는데 하지만 이는 리스트를순회하는 도중입니다. 즉 허용되지 않는 동작입니다. notifyElementAdded 메서드에서 수행하는 순회는 동기화 블럭 안에 있으므로 동시 수정이 발생하지 않도록 보장하지만, 자신이 콜백을 거쳐 되돌아가는 수정까지는 막지 못합니다.
public class Example {
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
// 값이 23이라면 자신을 구독해지
// this를 넘겨주어야하기 때문에 람다가 아닌 익명객체 사용
if (element == 23) set.removeObserver(this);
}
});
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
}
🧨 문제가 발생할 수 있는 상황 - 교착 상태
- 아래 예제코드를 수행하면 23까지 출력은 되지만 그 뒤에 교착상태에 빠지게 됩니다.
- 1 - try 문에 잇는 set.removeObserver 메서드를 호출하면 관찰자를 잠글려고 합니다.
- 2 - 하지만 락은 메인 메서드가 쥐고 있으므로 락을 얻을 수 없게됩니다.
- 3 - 이때 동시에 메인 스레드에서는 백그라운드 스레드가 관찰자를 제거하기를 기다리고 있습니다.
- 4 - 교착상태에 빠지게 됩니다.
- 물론 자바 언어에서는 락의 재진입을 허용하고 있기 때문에 교착상태에 빠지지 않을 수 있습니다. 이런 재진입 가능 락은 객체 지향 멀티스레드 프로그램을 쉽게 구현할 수 있게 해주지만, 응답 불가(교착 상태)가 될 상황을 안전 실패(데이터 훼손)로만 바꿔서 문제를 만들 수 있습니다.
public class Example {
public static void main(String[] args) {
ObservableSet<Integer> set = new ObservableSet<>(new HashSet<>());
set.addObserver(new SetObserver<Integer>() {
@Override
public void added(ObservableSet<Integer> set, Integer element) {
System.out.println(element);
if (element == 23) {
ExecutorService exec = Executors.newSingleThreadExecutor();
try {
// 여기서 lock이 발생하게 됩니다.(메인 스레드는 작업을 가리키고 있음)
exec.submit(() -> set.removeObserver(this)).get();
} catch (ExecutionException | InterruptedException e) {
throw new AssertionError(e);
} finally {
exec.shutdown();
}
}
}
});
for (int i = 0; i < 100; i++) {
set.add(i);
}
}
}
💡 해결방법
📜 콜백 메서드를 동기화 바깥으로 옮겨 해결
- 문제가 될 여지가 있는 코드를 동기화 블럭 바깥으로 옮겨 해결합니다.
public 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
- java,util.concurrent에서 제공하는 CopyOnWriteArrayList 클래스는 이런 문제를 해결하기 위해 설계된 객체로, ArrayList를 구현하되 내부를 변경하는 작업은 늘 복사본을 만들어 수행하도록 구현되었다. 다른 용도로는 느리지만, 순회가 대부분이고 간혹 수정할 일이 있는 관찰자 리스트 용도로는 적절합니다.
✔️ 정리
- 클라이언트가 넘겨준 함수는 동기화 영역 안에서 호출하면 안됩니다.
- 동기화 영역 안에서의 작업은 최소한으로 줄이는게 좋습니다.
- 가변 클래스를 설계할 때는 스스로 동기화가 필요한지 고민해야합니다.
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item82. 스레드 안전성 수준을 문서화하라 (0) | 2022.08.24 |
---|---|
이펙티브 자바 - Item81. wait와 notify보다는 동시성 유틸리티를 애용하라 (0) | 2022.08.23 |
이펙티브 자바 - Item78. 공유중인 가변 데이터는 동기화해 사용하라 (0) | 2022.08.21 |
이펙티브 자바 - Item73. 메서드가 던지는 모든 예외를 문서화하라 (0) | 2022.08.20 |
이펙티브 자바 - Item72. 표준 예외를 사용하라 (1) | 2022.08.20 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- spring boot redisson destributed lock
- 서비스 기반 아키텍처
- spring boot redisson sorted set
- JDK Dynamic Proxy와 CGLIB의 차이
- 공간 기반 아키텍처
- 자바 백엔드 개발자 추천 도서
- microkernel architecture
- 트랜잭셔널 아웃박스 패턴 스프링부트
- spring boot excel download paging
- pipeline architecture
- 람다 표현식
- spring boot redisson 분산락 구현
- polling publisher spring boot
- 레이어드 아키텍처란
- spring boot 엑셀 다운로드
- transactional outbox pattern spring boot
- service based architecture
- redis 대기열 구현
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- redis sorted set으로 대기열 구현
- space based architecture
- java ThreadLocal
- spring boot redis 대기열 구현
- redis sorted set
- spring boot poi excel download
- @ControllerAdvice
- pipe and filter architecture
- transactional outbox pattern
- java userThread와 DaemonThread
- spring boot excel download oom
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함