diff --git "a/item79/item79_\352\263\274\353\217\204\355\225\234 \353\217\231\352\270\260\355\231\224\353\212\224 \355\224\274\355\225\230\353\235\274_\354\212\271\355\227\214.md" "b/item79/item79_\352\263\274\353\217\204\355\225\234 \353\217\231\352\270\260\355\231\224\353\212\224 \355\224\274\355\225\230\353\235\274_\354\212\271\355\227\214.md" new file mode 100644 index 0000000..9500691 --- /dev/null +++ "b/item79/item79_\352\263\274\353\217\204\355\225\234 \353\217\231\352\270\260\355\231\224\353\212\224 \355\224\274\355\225\230\353\235\274_\354\212\271\355\227\214.md" @@ -0,0 +1,190 @@ +# 아이템 79 - 과도한 동기화는 피하라 + +### 과도한 동기화는 성능을 떨어뜨리고 , DeadLock에 빠뜨리고, 예측할 수 없는 오동작을 이르킨다. + +## 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다. + +동기화된 영역 안에서는 재정의할 수 있는 메서드는 호출하면 안 되며, 클라이언트가 넘겨준 함수 객체를 호출해서도 안 된다. 동기화된 영역을 포함한 클래스 관점에서는 이런 메서드는 모두 외계인이다. 그 메서드가 무슨 일을 할지 알지 못하며 통제할 수도 없다. `외계인 메서드(alien method)`가 하는 일에 따라 동기화된 영역은 예외를 일으키거나, 교착상태에 빠지거나 데이터를 훼손할 수도 있다. + +→ 외계인 메서드란? + +주로 동시성(concurrency) 환경에서 **동작을 예측하거나 통제할 수 없는 외부 메서드** + +### 동기화 블록에서의 외계인 메서드 호출 + +```java +public class ObservableSet extends ForwardingSet { + public ObservableSet(Set set) { super(set); } + + private final List> observers + = new ArrayList<>(); + + public void addObserver(SetObserver observer) { + synchronized(observers) { + observers.add(observer); + } + } + + public boolean removeObserver(SetObserver observer) { + synchronized(observers) { + return observers.remove(observer); + } + } + + private void notifyElementAdded(E element) { + synchronized(observers) { + for (SetObserver 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 c) { + boolean result = false; + for (E element : c) + result |= add(element); // notifyElementAdded를 호출한다. + return result; + } +} +``` + +어떤 집합을 감싼 래퍼 클래스이고, 이 클래스의 클라이언트는 집합에 원소가 추가되면 알람을 받는 관찰자 패턴이다 + +→ 관찰자들은 addObserver와 removeObserver 메서드를 호출해 구독을 신청하거나 해지한다. 두 경우 모두 다음 콜백 인터페이스의 인스턴스를 메서드에 건넨다. + +```java +public interface SetObserver { + // ObservableSet에 원소가 더해지면 호출된다. + void added(ObservableSet set, E element); +} +``` + +### 예외 발생 + +ObservableSet은 잘 작동할 것으로 보이지만, 클라이언트 코드가 다음과 같다면 어떨까? + +```java +public static void main(String[] args) { + ObservableSet set = + new ObservableSet<>(new HashSet<>()); + + set.addObserver(new SetObserver<>() { + public void added(ObservableSet 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 가 관찰자들의 리스트를 순회하는 도중이기 때문이다. + +observers.remove 메서드를 호출할 때, notifyElementAdded 메서드에서 이 리스트를 순회하는 도중이기 때문에 허용되지 않은 동작이다. notifyElementAdded 메서드 내의 동기화 블록은 동시 수정이 일어나지 않도록 보장하지만, 정작 콜백을 거쳐 되돌아와 수정하는 것까지 막지는 못한다. + +### 교착상태 + +이번엔 구독해지를 하는 관찰자가 removeObserver 를 직접 호출하지 않고 실행자 서비스(ExecutorService)를 사용해 다른 스레드에게 부탁하는 예를 보자. + +```java +public static void main(String[] args) { + ObservableSet set = + new ObservableSet<>(new HashSet<>()); + + // 쓸데없이 백그라운드 스레드를 사용하는 관찰자 + set.addObserver(new SetObserver<>() { + public void added(ObservableSet 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); +} +``` + +이 프로그램을 실행하면 예외는 발생하지 않지만 교착상태에 빠진다. + +백그라운드 스레드가 s.removeObserver 를 호출하면, 관찰자를 잠그려 하지만 락을 얻을 수 없다. notifyElementAdded 메서드를 실행 중이기 때문에 메인 스레드가 락을 쥐고 있기 때문이다. 그리고 메인 스레드는 백그라운드 스레드가 관찰자를 제거하기를 기다리고 있다. (교착상태) + +### 외계인 메서드 호출을 동기화 블록 바깥으로 옮기기 + +이런 문제는 외계인 메서드 호출을 동기화 블록 바깥으로 옮기면 해결할 수 있다. + +notifyElementAdded 메서드에서라면 관찰자 리스트를 복사해 쓰면 락 없이도 안전하게 순회할 수 있다. + +```java +private void notifyElementAdded(E element) { + List> snapshot = null; + synchronized(observers) { + snapshot = new ArrayList<>(observers); + } + for (SetObserver observer : snapshot) + observer.added(this, element); +} +``` + +이 방법 외에도 자바의 동시성 컬렉션 라이브러리의 `CopyOnWriteArrayList` 를 사용하는 방법도 있다. 이는 ArrayList 를 구현한 클래스로, 내부를 변경하는 작업은 항상 깨끗한 복사본을 만들어 수행하도록 구현되어 있다. 내부의 배열은 절대 수정되지 않으니 순회할 때 락이 필요 없어 매우 빠르다. 다른 용도로 쓰인다면 끔찍이 느리겠지만, 수정할 일이 드믈고 순회만 빈번히 일어나는 관찰자 리스트 용도로는 최적이다. + +```java +private final List> observers = + new CopyOnWriteArrayList<>(); + +public void addObserver(SetObserver observer) { + observers.add(observer); +} + +public boolean removeObserver(SetObserver observer) { + return observers.remove(observer); +} + +private void notifyElementAdded(E element) { + for (SetObserver observer : observers) + observer.added(this, element); +} +``` + +### 열린 호출 (open call) + +동기화 영역 바깥에서 호출되는 외계인 메서드를 열린 호출(open call) 이라고 한다. 외계인 메서드는 얼마나 오래 실행될지 알 수 없는데, 동기화 영역 안에서 호출된다면 그 동안 스레드는 보호된 자원을 사용하지 못하고 대기해야만 한다. 따라서 열린 호출은 실패 방지 효과 외에도 동시성 효율을 크게 개선해준다. + +**기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.** 락을 억고, 공유 데이터를 검사하고, 필요하면 수정하고, 락을 놓는다. + +### 과도한 동기화의 비용 + +멀티코어가 일반화된 오늘날, 과도한 동기화가 초래하는 진짜 비용은 락을 얻는 데 드는 CPU 시간이 아니다. + +바로 **경쟁하느라 낭비하는 시간**, 즉 병렬로 실행할 기회를 잃고 모든 코어가 메모리를 일관되게 보기 위한 지연시간이 진짜 비용이다. **가상머신의 코드 최적화를 제한**한다는 점도 숨겨진 비용이다. + +### 가변 클래스 지침 + +가변 클래스를 작성한다면 두 선택지 중 하나를 따르자. + +1. 동기화를 전혀 하지 말고, 그 클래스를 동시에 사용해야 하는 클래스가 외부에서 알아서 동기화하게 하자. +2. 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들자. 단, 클라이언트가 외부에서 객체 전체에 락을 거는 것보다 동시성을 월등히 개선할 수 있을 때만 선택해야 한다. + +선택하기 어렵다면 동기화하지 말고, 대신 문서에 "스레드 안전하지 않다"고 명시하자. + +만약 클래스 내부에서 동기화하기로 했다면, 락 분할(lock splitting), 락 스트라이핑(lock striping), 비차단 동시성 제어(nonblocking concurrency control) 등 다양한 기법을 동원해 동시성을 높여줄 수 있다. \ No newline at end of file diff --git "a/item83/\354\225\204\354\235\264\355\205\234 83 - \354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\354\236\220.md" "b/item83/\354\225\204\354\235\264\355\205\234 83 - \354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\354\236\220.md" new file mode 100644 index 0000000..a55dfc0 --- /dev/null +++ "b/item83/\354\225\204\354\235\264\355\205\234 83 - \354\247\200\354\227\260 \354\264\210\352\270\260\355\231\224\353\212\224 \354\213\240\354\244\221\355\236\210 \354\202\254\354\232\251\355\225\230\354\236\220.md" @@ -0,0 +1,198 @@ +# 아이템 83 - 지연 초기화는 신중히 사용하자 + +### 지연 초기화란 ? + +필드의 초기화 시점을 그 값이 처음 필요할때까지 늦추는 기법. + +→ 값이 전혀 쓰이지 않으면 초기화도 결코 일어나지 않는다. ( 정적 필드 + 인스턴스 필드 모두 사용 가능 ) + +최적화 용도로 사용하지만 클래스와 인스턴스 초기화 때 발생하는 위험한 순환 문제를 해결하는 효과도 있다. + +--- + +## 양날의 검 ‘ 지연 초기화 ‘ + +### 당연한 이점 + +→ 초기화를 지연시키므로 초기화 비용은 줄어든다. + +### 그치만 ? + +→ 지연 초기화하는 필드에 접근하는 비용이 커진다. + +그래서 초기화를 지연을 한다해서 성능적으로 이득만 볼 수는 없는 구조라는 것이다. + +--- + +## 그럼 이 ‘양날 의 검’ 을 언제 사용할까 ? + +1. 해당 클래스의 인스턴스 중 그 필드를 사용하는 인스턴스의 비율이 낮은데, 그 필드를 초기화 하는 비용이 크다면 지연 초기화를 쓴다. + +→ 근데 ? 필드를 초기화 하는 비용이 큰지 아닌지 어떻게 아냐 ? + +→ 실제로 적용하고 실행할때 까지는 모른다. ㅇㅇ → 성능 비교를 해봐야 한다. + +--- + +## 멀티스레드 환경의 지연초기화는 까다롭다! + +지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 **“ 동기화 “ 를 해줘야 한다.** + +그렇지 않으면 심각한 버그로 이어질 것이다. + +### 이게 무슨말이냐 + +→ 여러 스레드가 동시에 아직 안 만든 객체를 처음 쓰려고 하면, 동기화가 없을 경우 + +→ 객체가 2번 이상 생성됨 + +→ 완전히 초기화 되지 않은 상태를 다른 스레드가 보게 되는 문제가 생김 + +--- + +## 지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면 synchronized를 접근자를 사용하자. + +``` +근데 ? 초기화 순환성이 뭐야 ? + +A를 만들려면 B가 필요하고, +B를 만들려면 다시 A가 필요한 상황 + +객체 A 생성 중 → B 접근 +B 생성 중 → 다시 A 접근 +→ 무한 대기, NPE, 이상한 상태가 발생할 수 있음 +``` + +이 방법이 가장 간단하고 명확한 대안이다. + +```java +private FieldType field; + +private synchronized FieldType getFieldO { + if (field = null) + field = computeFieldValue(); + return field; +} +``` + +## 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 (lazy initialization holder class) 관용구를 사용하자. + +```java +private static class FieldHolder { + static final FieldType field = computerFieldValue(); +} + +private static FieldType getField() { return FieldHolder.field; } +``` + +- 이 FieldHolder 클래스는 처음엔 로딩 되지 않습니다. +- getField가 호출되는 순간 + - FieldHolder.field 를 읽으려고 함 + - 그때 JVM이 FieldHolder 클래스를 초기화 함 + - computerFieldValue() 를 실행함. + +다시말해 필드가 실제로 필요해질 때까지 초기화를 미루는 것. + +--- + +## 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사(doublecheck) 관용구를 사용하라. + +double check 를 쓰기 전에는 많은 생각이 있었을 것이다. + +- 지연 초기화 + synchronized 만 쓰면 + + → 매번 락을 잡음 → 성능 저하 + + +하지만 우리의 이상적인 목표는 ? + +→ 초기화 할때만 동기화 + +→ 초기화 끝난 뒤에는 락 없이 빠르게 접근 + +```java +private volatile FieldType field; + +private FieldType getField() { + FieldType result = field; // 첫 번째 검사 (락 없음) + if (result != null) { + return result; + } + + synchronized (this) { + if (field == null) { // 두 번째 검사 (락 있음) + field = computeFieldValue(); + } + return field; + } +} +``` + +이것의 동작 흐름은 + +→ 첫 번쨰 검사는 동기화 하지 않는다. + +→ 이미 초기화 되어 있으면 바로 반환 → 가장 빠름 + +field 가 null이면? + +- synchronized + +2 번째 검사 + +- 다른 스레드가 이미 초기화했을 수도 있으므로 다시 확인 +- 여전히 null일 때만 초기화 + +이코드의 예쁜점? + +초기화는 단 한번만 진행함. + +초기화 이후에는 락 비용이 0임 + +--- + +## `volatile` 이게 왜 반드시 필요한것인가 ? + +이유는 재정렬 + 가시성 문제 + +- `volatile` 이게 없으면 ? + - 한 스레드가 초기화 도중인 객체를 다른 스레드가 완성된 것 처럼 볼 수 있음 + +⭐ 이중 검사에서 volatile은 필수 → 초기화 순서를 보장하며 모든 스레드에 최신값을 보장한다. + +--- + +## 그럼 왜 지역변수 result 를 쓰는가 ? + +- field를 한 번만 읽게 하기 위해서 +- volatile 필드 읽기는 상대적으로 비쌈 +- 지역 변수는 CPU 레지스터에 캐시됨 + +→ 이미 초기화된 상황에서 필드 접근 비용을 최소화 할 수 있음 + +--- + +## 중복 초기화가 일어나도 괜찮을 때는 어떻게 할까 ? + +```java +private volatile FieldType field; + +private FieldType getField() { + FieldType result = field; + if (result == null) + field = result = computeFieldValue(); + return result; +} +``` + +결과가 동일하고 부작용이 없을 때만 사용 가능 + +--- + +**지연 초기화는 기본적으로 피하되 불가피하다면 인스턴스 필드는 이중검사, 정적 필드는 홀더 클래스를 사용하라.** + +--- + +### 핵심정리 + +대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다. 성능 때문에 혹은 위험한 초기화 순환을 막기 위해 꼭 지연 초기화를 써야 한다면 올바른 지연 초기화 기법을 사용 하자. 인스턴스 필드에는 이중검사 관용구를, 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하자. 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다. \ No newline at end of file