엄범

 

아이템 78. 공유 중인 가변 데이터는 읽기 쓰기 모두 동기화해 사용하라
  • lock을 걸어서 sync하는건 성능에 좋지 않으니까, 원자적 데이터를 읽고 쓸 때는 동기화하지 말아야 겠다고 생각하기 쉬운데, 아주 위험한 발상이다.
    • 물론 원자적 데이터(boolean, long, ...)를 읽고 쓰는 동작은 atomic하므로, 배타적으로 수행된다.
      • 배타적으로 수행된다는건, 읽고 쓰는 중 데이터가 훼손되거나 반영이 되지 않는 문제가 없다는 것이다.
    • 그러나 동기화의 기능은 두 가지다.
      • 1. 배타적 수행
      • 2. 스레드 간 통신(한 스레드가 만든 변화를 다른 스레드가 확인할 수 있도록 적용)
    • 원자적 데이터를 읽고 쓰는건 배타적 수행은 만족하지만, 해당 스레드가 만든 변화를 다른 스레드에 공개하는건 보장하지 않는다.
      • 이게 뭔소리냐? 어차피 객체는 Heap에 저장될테고, Heap은 스레드들이 공유하는 영역이니까 한 스레드가 객체를 바꾸면, 다른 스레드에서 그 객체에 접근해도 변경된 값이 적용돼 나와야 하는거 아니냐? 라고 생각할 수 있다.
      • chapter11/item78/brokenstopthread/StopThread.java
        • 이 프로그램은 1초 뒤에 종료될 것 같지만 종료되지 않는다.
        • 왜냐, JVM이 다음과 같이 hoisting하면서 최적화 하기 때문이다!

```java

// 원래 코드

while (!stopRequested)

    i++;

 

// 최적화된 코드

if (!stopRequested)

    while (true)

        i++;

```

  • 책에서 `` volatile`` 한정자도 소개하고 있다.
    • 이는 배타적 수행과는 상관없지만 가장 최근에 기록된 값을 읽게 됨을 보장(스레드 간 통신)하는 키워드다.
    • 배타적 수행을 보장하지 않으므로 쓰기를 수행하는 스레드가 2개 이상이면 `` volatile``을 쓰면 안된다.
    • 이런 경우 `` synchronized``가 필요한데, 동기화 보다는 `` AtomicLong`` 등을 사용하는 편이 낫다
  • 그러니, `` AtomicLong`` 등을 사용하자!
  • 더불어 lock 필드는 항상 ``java final``로 선언하자.

 

아이템 79. 과도한 동기화는 피하라
  • 동기화 블록 안에서 내가 100% 제어할 수 없는 코드를 호출해서는 안된다. Exception이나 데드락이 발생할 수 있음
    • 람다 등으로 받은 클라이언트의 코드를 호출해서는 안된다.
    • Override 할 수 있는 메서드를 호출해서는 안된다.
  • 이런 코드는 동기화 블록 밖으로 옮기고, 동기화 블록에서는 가능한 한 일을 적게 하는 것이 좋다.
  • `` CopyOnWriteArrayList`` 등 동기화 안해도 되는 자료구조를 고려할 것.

 

아이템 80. Thread보다는 Executor, Task, Stream을 애용하라
  • 스레드 직접 쓰지 말고, 다음의 고수준 API를 사용할 것
  • `` ExecutorService``
  • `` ThreadPoolExecutor``
    • `` CachedThreadPool``
      • 현재 모든 스레드가 돌고 있는데 새로운 요청이 들어오면, 큐에 쌓는게 아니라 즉시 새로운 스레드 하나 띄워서 실행함. 요청이 많다면 스레드가 아주 많이 뜰 수도 있다.
    • `` FixedThreadPool``
      • 스레드 수가 고정. 넘치면 큐에 쌓는다.
  • Stream 병렬 처리는 내부적으로 `` ForkJoinPool``을 사용하고 있음. 좋음.

 

아이템 81. wait와 notify는 사용하기 어려우니 고수준 동시성 유틸리티를 애용하라
  • HashMap vs HashTable vs ConcurrentHashMap
    • 단일 스레드 환경이라면 HashMap
    • 멀티 스레드 환경이라면 ConcurrentHashMap을 사용한다.
    • HashTable은 전체적으로 synchronized하는 방식이라 퍼포먼스가 느리고 ConcurrentHashMap 대비 별 장점이 없음.
  • 여러 기본적 동작을 하나의 원자적 동작으로 묶는 '상태 의존적 수정' 메서드들
    • ``java putIfAbsent()``
    • ``java compute()``
    • ``java computeIfPresent()``
    • ``java getOrDefault()``
    • ``java map.remove()``
      • 함수는 해당 데이터를 맵에서 제거하면서 그 객체를 리턴해준다! 그래서 별도로 get()을 따로 쓸 필요가 없다!
    • Map.computeIfAbsent()

```java

Map<String, Set> groups = new HashMap<>();

if (groups.get(alphabetize(word)) == null) {

    groups[alphabetize(word)] = new TreeSet<>();

}

groups.get(alphabetize(word)).add(word);

```

```java

Map<String, Set> groups = new HashMap<>();

groups.computeIfAbsent(alphabetize(word), (_) -> new TreeSet<>())  // return값인 new TreeSet이 자동으로 map에 추가된다.

      .add(word);

```

  • 동기화 장치 리스트
    • `` CountDownLatch, Semaphore, CyclicBarrier, Exchanger, Phaser``

 

아이템 82. 스레드 안전성 수준을 문서화하라
  • 문서화 방법론은 책 참고

 

아이템 83. 지연 초기화는 신중히 사용하라
  • 지연 초기화가 무조건 좋은게 아니다.
    • 인스턴스 생성 시의 초기화 비용은 줄지만,
    • 그 대신 지연 초기화 대상 필드에 접근하는 비용은 커진다!
    • 그래서 실제로는 지연 초기화가 성능을 느려지게 할 수도 있다.
  • 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
  • 상황에 따른 지연 초기화 패턴에 대해서는 책을 참고.
    • 초기화 순환성을 깨뜨릴 것 같다면 synchronized를 단 접근자
    • 성능 문제 & 정적 필드라면 lazy initialization holder class 관용구 (singleton에서 사용하는 그 것)
    • 성능 문제 & 인스턴스 필드라면 volatile을 사용하는 double-check 관용구

 

아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라
  • 특히 주의해야 할 점이 스레드가 busy waiting 하면 안된다는 점.
    • 공유 객체의 상태가 바뀔 때 까지 계속 while돌면서 검사하는건 리소스 낭비
    • `` CountDownLatch``는 내부적으로 ``java LockSupport.parkNanos(this, nanosTimeout);``를 사용하고 있긴 함.
  • 뭔가 문제가 생겼을 때 스레드 우선순위로 해결하려는 생각은 좋지 못함. 스레드 우선순위를 조정해야 하는 상황은 정말 드물고 이식성이 떨어짐.