Post

(Effective Java) 11장 동시성 + collection 유틸 메서드

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

  • [Thread-safety] shared mutable state 관리
  • 개인의견 ) 케이스에 따라 적절한 동기화 수준이 다를 수 있다…만 아무튼 shared mutable state가 있다면 항상 동시성 문제 해결 전략이 필요한 것은 맞다.
  • 더불어 lock을 사용할거라면 lock 필드는 항상 final로 선언하자.

아이템 79. 과도한 동기화는 피하라

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

아이템 80. Thread보다는 Executor, Task, Stream을 애용하라

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

아이템 81. wait와 notify는 사용하기 어려우니 고수준 동시성 유틸리티를 애용하라

  • Thread safe한 HashMap의 원리
  • HashMap vs HashTable vs ConcurrentHashMap
    • 단일 스레드 환경이라면 HashMap
    • 멀티 스레드 환경이라면 ConcurrentHashMap을 사용한다. (single writer 이더라도, 스펙에 single writer일 때는 안전하다 라는 얘기가 없으므로 ConcurrentHashMap 사용하는게 맞다고 생각됨.)
    • HashTable은 전체적으로 synchronized하는 방식이라 퍼포먼스가 느리고 ConcurrentHashMap 대비 별 장점이 없음.
    • 상대적으로 느리지만, 순서를 보장해주는 ConcurrentSkipListMap 도 있다.
  • 여러 기본적 동작을 하나의 원자적 동작으로 묶는 ‘상태 의존적 수정’ 메서드들
    • putIfAbsent()
    • compute()
    • computeIfPresent()
    • getOrDefault()
    • map.remove()
      • 함수는 해당 데이터를 맵에서 제거하면서 그 객체를 리턴해준다! 그래서 별도로 get()을 따로 쓸 필요가 없다!
    • Map.computeIfAbsent()
1
2
3
4
5
Map<String, Set> groups = new HashMap<>();
if (groups.get(alphabetize(word)) == null) {
    groups[alphabetize(word)] = new TreeSet<>();
}
groups.get(alphabetize(word)).add(word);
1
2
3
4
Map<String, Set> groups = new HashMap<>();
groups.computeIfAbsent(alphabetize(word), (_) -> new TreeSet<>())
                      // 람다 return값인 new TreeSet이 자동으로 map에 insert 된다.
      .add(word);     // insert된 TreeSet이 반환되어 바로 .add 가능
  • 동기화 장치 리스트
    • CountDownLatch, Semaphore, CyclicBarrier, Exchanger, Phaser

아이템 82. 스레드 안전성 수준을 문서화하라

  • 문서화 방법론은 책 참고

아이템 83. 지연 초기화는 신중히 사용하라

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

아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라

  • 특히 주의해야 할 점이 스레드가 busy waiting 하면 안된다는 점.

    • 공유 객체의 상태가 바뀔 때 까지 계속 while돌면서 검사하는건 리소스 낭비
    • CountDownLatch는 내부적으로 LockSupport.parkNanos(this, nanosTimeout);를 사용하고 있긴 함.
  • 뭔가 문제가 생겼을 때 스레드 우선순위로 해결하려는 생각은 좋지 못함. 스레드 우선순위를 조정해야 하는 상황은 정말 드물고 이식성이 떨어짐.

This post is licensed under CC BY 4.0 by the author.