아이템 78. 공유 중인 가변 데이터는 동기화해 사용하라
- 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
- 안정적인 통신에는 volatile을 사용하면 된다. (배타적 실행은 보장 x)
예: ++ 연산자 (하나의 연산으로 보이지만 사실은 조회, 증가 후 저장)
두 번째 스레드가 첫 번째 스레드의 연산 사이에 들어와 공유 필드를 읽게 되면, 첫 번째 스레드와 같은 값을 보게 됨 → safety failure
synchronized
사용하면 배타적 실행, 안정적 통신 모두 보장된다.
이유: 한번에 한 스레드만 해당 블록 수행, 블록 들어가기 전 동기화
- Atomic은 더 좋다.
동기화가 아닌 CAS(Compared and swap) 알고리즘으로 동작하여 원자성 보장
- 가변 데이터는 단일 스레드에서만 사용하자
아이템 79. 과도한 동기화는 피하라
-
동기화 블럭 안에서 절대 클라이언트에게 제어를 양도하면 안된다.
외계인 메서드 (ex. 재정의 가능 메서드, 클라이언트가 넘겨준 함수 객체)를 호출하면 안됨 → 교착 상태, 데이터 훼손 위험
-
자바 언어의 락은 재진입을 허용함
한 쓰레드가 synchronized 블록에 진입하며 모니터 객체의 락을 획득하면, 쓰레드는 자신이 획득한 모니터 객체에 대한 다른 synchronized 블록으로도 진입할 수 있음
-
동기화 영역에서 가능한 작은 작업을 하자
-
가변 클래스 작성 시 둘중에 꼭 하자
1. 동기화를 전혀 하지 말고, 그 클래스를 사용해야 하는 클래스가 알아서 동기화하게 한다. (java.util)
2. 동기화를 내부에서 수행해 스레드 안전한 클래스로 만든다. (java.util.concurrent)
-
이전 회사 동료가 synchronized 동작 관련 가르침을 주셔서 관련 내용 정리
내가 참고했던 stackoverflow글
https://stackoverflow.com/questions/8892880/how-to-synchronize-access-on-a-static-field-of-a-super-class
이 글을 보고 잘못 이해하여 잘못된 요약을 남겼는데 동료분께서 바로 잡아주셨다. (정말 감사합니다🥹)
stackoverflow의 예시에서 static 변수를 lock으로 사용한 것은 제어하려는 static 변수와의 scope를 맞춰주기 위함이고요!
(만약 오브젝트 락을 걸게되면 서로다른 인스턴스에서는 static 변수에 대한 접근이 가능하기 때문에 동시성 이슈 발생)
class lock을 걸어주지 않은 이유는 또 다른 class lock을 사용하는 메서드가 존재할 경우 락을 획득하기 전까지 블록되기 때문이에요!
해당 클래스의 필드 및 메서드의 접근 자체와는 상관없고 락을 획득하려는 행위 자체에만 유효합니다!
- synchronized 내부 동작과 java object memory layout
실제로 lock이 걸릴 때는 인스턴스 메모리 내부의 header부분에 thread 주소값으로 lock이 설정됩니다. (lock의 종류에는 biased, thin, fat 등등 있음, 이를 구분하는 기준은 쓰레드 사이의 경쟁에 따라서 종류가 나뉘어짐)
이미지를 보면 header에 thin lock으로 설정된 것을 볼 수 있음
아이템 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라
- 동시성 작업을 할 때는 작업 큐를 직접 생성할 수도 있겠지만, 복잡한 작업들(안전실패, 응답불가 예방)이 필요하기 때문에 java.util.concurrent 패키지의 실행자, 태스크, 스트림을 이용하는 편이 더 낫다.
Executors 가 제공하는 정적팩터리 메서드를 사용하면 다양한 작업 큐를 얻을 수 있다.
- 실행자 프레임워크를 사용하면 작업단위(Task - Runnable, Callable)와 실행 매커니즘을 분리할 수 있다.
- 포크-조인 태스크는 먼저 일을 끝낸 스레드가 다른 스레드의 남은 태스크를 수행할 수 있어, CPU 활용률이 높아진다. 병렬스트림..!
아이템 81. wait와 notify보다는 동시성 유틸리티를 애용하라
- wait와 notify는 올바르게 사용하기 어렵기 때문에 고수준의 동시성 유틸리티를 사용하자.
실행자 프레임워크, 동시성 컬렉션(concurrent collection), 동기화 장치(synchronizer)
- 동시성 컬렉션은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션 (동기화한 컬렉션보다 훨씬 좋음)
동시성 무력화 불가능, 외부에서 락 추가로 사용하면 성능 나빠짐 → 여러 메서드를 원자적으로 묶어 호출할 수 없어서 상태 의존적 수정 메서드들이 추가됨 (ex. Map의 putIfAbsent(key, value)
- 동기화 장치는 스레드가 다른 스레드를 기다릴 수 있게 하여, 서로 작업을 조율할 수 있게 해준다.
가장 자주 쓰이는 CountDownLatch, Semaphore
가장 강력한 Phaser
- wait, notify는 뭐 이제는 거의 쓸 일 없다. 쓸 일 생기면 다시 봐야징
아이템 82. 스레드 안전성 수준을 문서화하라
- 멀티스레드 환경에서도 api를 안전하게 사용하려면 클래스가 지원하는 스레드 안전성 수준을 명시해야 한다.
immutable
: 무조건적 스레드 안전
unconditionally thread-safe
: 조건부 스레드 안전
conditionally thread-safe
: 스레드 안전하지 않음
not thread-safe
: 스레드 적대적(thread-hostile)
- 클래스가 외부에서 사용할 수 있는 락을 제공하면 클라이언트에서 일련의 메서드 호출을 원자적으로 수행할 수 있다.
하지만 고성능 동시성 제어 메커니즘(ex.동시성 컬렉션)과 혼용 불가, 서비스 거부 공격의 위험이 있다.
- 비공개 락 객체
무조건적 스레드 안전 클래스에서만 사용 가능
락 교체되면 끔찍하므로 무조건 final로 선언
상속용으로 설계한 클래스에 특히 좋음
아이템 83. 지연 초기화는 신중히 사용하라
- 지연 초기화 : 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
- 대부분의 상황에서 일반 초기화가 더 낫다.
- 지연 초기화가 필요할 때
인스턴스 중 그 필드를 사용하는 인스턴스 비율은 낮고, 해당 필드를 초기화하는 비용은 높을 때
하지만 멀티스레드 환경에서는 까다로움
- 지연 초기화 방법
1. synchronized를 단 접근자를 사용
2. 정적 필드에 대해선 지연 초기화 홀더 클래스 관용구 사용
3. 인스턴스 필드에 대해선 이중 검사 관용구 사용 (해당 필드는 반드시 volatile 선언)
아이템 84. 프로그램의 동작을 스레드 스케줄러에 기대지 마라
- 여러 스레드가 실행 중이면 OS의 스레드 스케줄러가 어떤 스레드가 얼마나 오래 실행할지 정한다. 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.
- 전체 스레드 수 = 대기중인 스레드 수 + 실행 가능한 스레드 수
- 당장 처리해야할 작업이 없다면 스레드가 실행되면 안된다.
실행 가능한 스레드 수를 적게 유지하는 것이 좋다.
절대 busy waiting 상태가 되면 안된다.
- Thread.yield 쓰지 말자. 우선순위 조절도 ㄴㄴ