[Effective Java] 아이템 79. 과도한 동기화는 피하라

2022. 12. 20. 22:54· BE/Java
목차
  1. 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
  2.  
반응형

EFFECTIVE JAVA(이펙티브 자바)

 

이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.


아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라

 

아이템 78에서는 동기화의 필요성을 다뤘다면, 이번에는 동기화의 남용에 대해 다룬다.

 

동기화 유의점

  • 과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠트리며 예측할 수 없는 동작을 야기할 수도 있다.
  • 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
    • 안전실패 : 프로그램이 잘못된 결과를 계산하는 것
    • 예로, 동기화된 영역 안에서는 재정의할 수 있는 메서드는 호출하면 안되며, 클라이언트가 넘겨준 함수 객체를 호출해서는 안된다.
    • 동기화된 영역을 포함한 클래스 관점에서는 이런 메서드는 모두 바깥 세상에서 온 외계인이며, 그 메서드가 무슨 일을 하는 지 모르고 통제할 수도 없다. (외계인 메서드)
    • 이러한 외계인 메서드에 따라 동기화된 영역은 예외, 교착상태를 일으키거나 데이터를 훼손할 수도 있다.

 

 

ex 1) 동기화 블록 안에서 외계인 메서드를 호출하는 잘못된 예

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;
    }
}
  • 어떤 집합(Set)을 감싼 래퍼 클래스인데, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알림을 받을 수 있는 예제이다.
  • 관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지한다.
  • 두 경우 모두 다음 콜백 인터페이스의 인스턴스를 메시지에 건넨다.

 

 

ex 2) 커스텀 함수형 인터페이스

@FunctionalInterface public interface SetObserver<E> {
	// ObservableSet에 원소가 더해지면 호출된다.
	void added(ObservableSet<E> set, E element);
}
  • 위의 커스텀 함수형 인터페이스를 정의한 이유는 이름이 더 직관적이고 다중 콜백을 지원하도록 확장할 수 있기 때문이다.

 

 

ex 3) observableSet을 사용한 main 메서드 예

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

    set.addObserver((set1, element) -> System.out.println(element));

    for (int i = 0; i < 100; i++)
        set.add(i);
}
  • 위의 예제는 0부터 99까지 출력한다.

 

 

ex 4) 다른 익명함수를 넘기는 예

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

    set.addObserver(new SetObserver<>() {
        @Override
        public void added(ObservableSet<Integer> s, Integer e) {
            System.out.println(e);
            if (e == 23) // 값이 23이면 자기 자신을 제거
                s.removeObserver(this);
        }
    });

    for (int i = 0; i < 100; i++)
        set.add(i);
}
  • 위의 예에 대한 기대는 0부터 23까지 출력한 후 관찰자 자신을 구독해지한 다음 조용히 종료할 것이다.
  • 하지만, 실제로는 23까지 출력한 다음 ConcurrentModificationException을 던진다.
  • 관찰자의 added 메서드 호출이 일어난 시점이 notifyElementAdded가 관찰자들의 리스트를 순회하는 도중이기 때문이다.
  • added 메서드는 ObserableSet의 remove Observer 메서드를 호출하고, 이 메서드는 다시 observers.remove메서드를 호출하는데, 리스트에서 원소를 제거할 때 이 리스트는 이미 순회하는데 사용되는 것이다.
  • 즉, notifyElementAdded 메서드에서 수행하는 순회는 동기화 블록 안에 있으므로 동시 수정이 일어나지 않도록 보장하지만, 정작 자신이 콜백을 거쳐 되돌아와 수정하는 것까지는 막지 못한다.

 

 

ex 5) 교착상태를 일으키는 예

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();
            }
        }
    }
});
  • 이 예는 예외는 발생하지 않지만, 교착상태에 빠진다.
  • 백그라운드 스레드가 s.removeObserver를 호출하면 관찰자를 잠그려 시도하지만 락을 얻을 수 없다.
  • 메인 스레드가 이미 락의 주체이기 때문이다.
  • 그와 동시에 메인 스레드는 백그라운드 스레드가 관찰자를 제거하기를 기다린다. 즉, 서로 기다린다.
  • 실제 시스템에서도 이렇게 동기화된 영역 안에서 외계인 메서드를 호출하여 교착상태에 빠지는 사례가 자주 있다.
  • 해결 방법
    • 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 해결할 수 있다.
    • java.util.concurrent.CopyOnWriteArrayList 사용

 

동기화 팁

  • 기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.
  • 가변 클래스를 작성하는 경우 아래 두 가지 중 하나를 따르자.
    • 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 동기화 하도록 한다.
    • 동기화를 내부에서 수행해 스레드 안전한 클래스로 만든다. (단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때)

 

정리

  • 교착상태와 데이터 훼손을 피하려면 동기화 영역 안에서 외계인 메서드를 절대 호출하지 말자.
  • 동기화 영역 안에서의 작업은 최소한으로 줄이자.
  • 가변 클래스를 설계할 때는 스스로 동기화해야 할 지 고민하자.
  • 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부를 문서에 밝히자.
반응형

'BE > Java' 카테고리의 다른 글

[Effective Java] 아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라  (0) 2022.12.22
[Effective Java] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 이용하라  (0) 2022.12.21
[Effective Java] 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라  (0) 2022.12.19
[Effective Java] 아이템 77. 예외를 무시하지 말라  (0) 2022.12.16
[Effective Java] 아이템 76. 가능한 한 실패 원자적으로 만들라  (0) 2022.12.16
  1. 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
  2.  
'BE/Java' 카테고리의 다른 글
  • [Effective Java] 아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라
  • [Effective Java] 아이템 80. 스레드보다는 실행자, 태스크, 스트림을 이용하라
  • [Effective Java] 아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
  • [Effective Java] 아이템 77. 예외를 무시하지 말라
멍목
멍목
개발 관련 새롭게 알게 된 지식이나 좋은 정보들을 메모하는 공간입니다.
반응형
멍목
김멍목의 개발블로그
멍목
전체
오늘
어제
  • 분류 전체보기 (514)
    • BE (190)
      • Spring (21)
      • Java (141)
      • Kotlin (6)
      • JPA (22)
    • FE (33)
      • Javascript (16)
      • Typescript (0)
      • React (5)
      • Vue.js (9)
      • JSP & JSTL (3)
    • DB (32)
      • Oracle (22)
      • MongoDB (10)
    • Algorithm (195)
    • Linux (8)
    • Git (6)
    • etc (42)
    • ---------------------------.. (0)
    • 회계 (4)
      • 전산회계 2급 (4)
    • 잡동사니 (2)

블로그 메뉴

  • 홈
  • 관리

공지사항

인기 글

태그

  • JPA 공부
  • 이펙티브자바
  • 전산회계 2급 준비
  • MongoDB 기초부터 실무까지
  • 이펙티브 자바
  • 자기 공부
  • 더 자바 애플리케이션을 테스트하는 다양한 방법
  • 자기공부
  • 코틀린
  • 프로젝트로 배우는 Vue.js 3
  • 자바공부
  • 알고리즘공부
  • 코테 공부
  • 자바 공부
  • Oracle
  • 코테공부
  • java 8
  • vue3 공부
  • MongoDB 공부
  • 더 자바 Java 8
  • 자바 개발자를 위한 코틀린 입문
  • 자바 테스팅 프레임워크
  • 자기개발
  • junit5
  • JPA
  • MongoDB with Node.js
  • Java to Kotlin
  • Effective Java
  • 자기 개발
  • 알고리즘 공부

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
멍목
[Effective Java] 아이템 79. 과도한 동기화는 피하라
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.