(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.