[Java]동시성
Java 에서의 동시성 - Effective Java & Clean Code 내용 정리
🎮 Effective Java
Item 78. 공유 중인 가변 데이터는 동기화해 사용
synchronized
: 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장.
동기화는 일관성이 깨진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
동기화는 배타적 실행
뿐 아니라 스레드 사이의 안정적인 통신
에 꼭 필요하다.
쓰기와 읽기 모두가 동기화 되지 않으면 동작을 보장하지 않는다.
동기화의 두 가지 기능
- 배타적 실행
- 스레드 간 통신
필드를 volatile
으로 선언하면 동기화를 생략해도 된다. volatile 한정자는 배타적 수행과는 상관없지만 항상 최근에 기록된 값을 읽게 됨을 보장한다.
java.util.concurrent.atomic
패키지에는 락없이도 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨 있다. volatile은 동기화의 통신 쪽만 지원하지만 이 패키지는 원자성(배타적 실행)까지 지원한다. 성능도 동기화 버전보다 우수하다.
가변 데이터는 단일 스레드에서만 쓰도록 하자
Item 79. 과도한 동기화는 피하라
과도한 동기화는 성능을 떨어뜨리고, 교착상태에 빠뜨리고, 심지어 예측할 수 없는 동작을 낳기도 한다.
응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다.
동기화 영역 안에서 외계인 메스드를 절대 호출하지 말자. 동기화 영역 안에서의 작업은 최소한으로 줄이자.
Item 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라
java.util.concurrent.Executors 를 사용하자
Itme 81. wait, notify 보다는 동시성 유틸리티를 애용하라
wait, notify 는 올바르게 사용하기가 아주 까다로우니 고수준 동시성 유틸리티를 사용하자.
java.util.concurrent의 세 범주
- 실행자 프레임워크
- 동시성 컬렉션(concurrent collection)
- 동기화 장치(synchronizer)
- 실행자 프레임워크
- Executors
- ThreadPoolExecutor : 스레드 풀 동작을 결정하는 거의 모든 속성 설정 가능. 스레드 완전 통제 가능.
- Executors.newCachedThreadPool : 작은 프로그램이나 가벼운 서버에 적합. 특별히 설정할 게 없고 일반적인 용도에 적합하게 동작. 요청받은 태스크들이 큐에 쌓이지 않고 즉시 스레드에 위임돼 실행.
- Executors.newFixedThreadPool : 무거운 프로덕션 서버에 적합. 스레드 개수를 고정.
- 동시성 컬렉션
- List, Queue, Map같은 표준 컬렉션 인터페이스에 동시성을 가미해 구현한 고성능 컬렉션
- 동시성 컬렉션에서 동시성을 무력화하는 건 불가능. 외부에서 락을 추가로 사용하면 오히려 속도가 느려짐
- ConcurrentHashMap : 동시성이 뛰어나며 속도도 무척 빠르다. Conllections.synchronizedMap 보다는 ConcurrentHashMap 을 사용하는게 훨씬 좋다.
- BlockingQueue : 작업이 성공적으로 완료될 때 까지 기다리도록 확장됨. ThreadPoolExecutor를 포함한 대부분의 실행자 서비스 구현체에서 BlockingQueue를 사용.
- 동기화 장치
- 스레드가 다른 스레드를 기다릴 수 있게 하여, 서로 작업을 조율할 수 있게 해준다
- CoundDownLatch : 일회성 장벽으로, 하나 이상의 스레드가 또 다른 하나 이상의 스레드 작업이 끝날 때 까지 기다리게 한다.
wait은 while문 안에서 호출하도록 하자. 일반적으로 notify 보다는 notifyAll을 사용해야 한다. 혹시라도 notify를 사용한다면 응답 불가 상태에 빠지지 않도록 각별히 주의하라.
Item 82. 스레드 안정성 수준을 문서화 하라
- 불변 immutable : 클래스의 인스턴스는 상수와 같아 외부 동기화 불필요
- String, Long, BigInteger
- 무조건적 스레드 안전 unconditionally thread-safe : 클래스의 인스턴스는 수정될 수 있으나, 내부에서 충실히 동기화해 별도의 외부 동기화 없이 동시에 사용해도 안전
- AtomicLong, ConcurrentHashMap
- 조건부 스레드 안전 conditionally thread-safe : 무조건적 스레드 안전과 같으나, 일부 메서드는 동시에 사용하려면 외부 동기화 필요
- Collections.synchronized 래퍼 메서드가 반환한 컬렉션들
- 스레드 안전하지 않음 not thread-safe : 클래스의 인스턴스 수정 가능. 동시에 사용하려면 각각의 메서드 호출을 클라이언트가 선택한 외부 동기화 메커니즘으로 감싸야 한다.
- ArrayList, HashMap 등으 기본 컬렉션
- 스레드 적대적 thread-hostile : 모든 메서드 호출을 외부 동기화롤 감싸더라도 멀티스레드 환경에서 안전하지 않다.
Item 83. 지연 초기화*는 신중히 사용하라
❗️지연 초기화 : 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
지연 초기화하는 필드를 둘 이상의 스레드가 공유한다면 어떤 형태로든 반드시 동기화해야 한다.
대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면 synchronized 를 단 접근자
를 사용하자
성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구
를 사용하자
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }
성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구
를 사용하자
private volatile FieldType field;
private FieldType getField() {
FiledType result = field;
if (result != null) { // 첫 번째 검사 (락 사용 안함)
return result;
synchronized(this) { // 두 번째 검사 (락 사용)
if (field == null)
field = computeFieldValue();
return field;
}
}
}
반복해서 초기화해도 상관없는 인스턴스 필드를 지연 초기화할 경우 이중검사에서 두 번째 검사를 생략한 단일검사 관용구
를 사용하자.
private volatile FieldType field;
private FieldType getField() {
FiledType result = field;
if (result == null)
field = result = computeFieldValue();
return result;
}
Item 84. 프로그램의 동작을 스레드 스케줄러에 기대지 말라
정확성이나 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.
Thread.yield와 스레드 우선순에 의존해서도 안 된다.
🎮 Clean Code 동시성 - 브레트 L. 슈허트(Brett L. Schuchert)
- 여러 스레드를 동시에 돌리는 이유
- 여러 스레드를 동시에 돌리는 어려움
- 대처 코드 작성 방법
- 동시성 테스트 방법 & 문제점
1. 여러 스레드를 동시에 돌리는 이유
동시성은 결합을 없애는 전략.
무엇
과언제
를 분리(구조적 개선)하는 전략. 응답 시간과 작업 처리량 개선.
무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다. (eg. Servlet)
미신과 오해
- 동시성은 항상 성능을 높여 준다
- 동시성은
때로
성능을 높여준다.
- 동시성은
- 동시성을 구현해도 설계는 변하지 않는다
- 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
- 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다
- 컨테이너 동작 방식, 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다.
동시성과 관련된 타당한 생각
- 동시성은 다소 부하를 유발
- 동시성은 복잡
- 일반적으로 동시성 버그는 재현하지 어렵다
- 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다
2. 여러 스레드를 동시에 돌리는 어려움
두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많은데, 그중에서 일 부경로가 잘못된 결과를 내놓음. 정확한 경로를 알려면 JIT(Just-In-Time) 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소 단위를 알아야 한다.
3. 대처 코드 작성 방법
동시성 방어 원칙
단일 책임 원칙 : 주어진 메서드/클래스/컴포넌트를 변경할 이유가 하나여야 한다는 원칙
동시성 관련 코드는 다른 코드와 분리해야 한다
- 자료 범위를 제한하라
자료를 캡슐화하라. 공유 자료를 최대한 줄여라 - 자료 사본을 사용하라
공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다 - 스레드는 가능한 독립적으로 구현하라
다른 스레드와 자료를 공유하지 않는다.
독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할 하라
라이브러리를 이해하라
- 스레드 환경에 안전한 컬렉션을 사용. 자바 5부터 제공
- 서로 무관한 작업을 수행할 때는 executor 프레임워크 사용
- 가능하다면 스레드가 차단(blocking)되지 않는 방법을 사용
- 일부 클래스 라이브러리는 스레드에 안전하지 못함
- 스레드 환경에 안전한 컬렉션
java.util.concurrent, java.util.concurrent.atomic, java.util.concurrent.locks를 익혀라
ReentrantLock | 한 메서드에서 잠그고 다른 메서드에서 푸는 락 |
Semaphore | 전형적인 세마포. 개수가 있는 락 |
CountDownLatch | 지정한 수만큼 이벤트가 발생하고 나서야 대기 중인 스레드를 모두 해제 하는 락. 모든 스레드에게 동시에 공편하게 시작할 기회를 줌 |
실행 모델을 이해하라
기본 용어
한정된 자원 Bound Resource | 다중 스레드 환경에서 사용하는 자원으로, 크기나 숫자가 제한적. DB 연결, 길이가 일정한 읽기/ 쓰기 버퍼 등이 예. |
상호 배제 Mutual Exclusion | 한 번에 한 스레드만 공유 자료나 공유 자원을 사용할 수 있는 경우 |
기아 Starvation | 한 스레드나 여러 스레드가 굉장히 오랫동안 혹은 영원히 자원을 기다린다. 예를 들어, 항상 짧은 스레드에게 우선순위를 준다면, 짧은 스레드가 지속적으로 이어질 경우, 긴 스레드가 기아 상태에 빠진다. |
데드락 Deadlock | 여러 스레드가 서로가 끝나기를 기다린다. 모든 스레드가 각기 필요한 자원을 다른 스레드가 점유하는 바람에 어느 쪽도 더 이상 진행하지 못한다. |
라이브락 Livelock | 락을 거는 단계에서 각 스레드가 서로를 방해한다. 스레드는 계속해서 진행하려 하지만, 공명(resonance)으로 인해, 굉장히 오랫동안 혹은 영원히 진행하지 못한다. |
다중 스레드 실행 모델
- 생산자-소비자
- 읽기-쓰기
- 식사하는 철학자들
동기화하는 메서드 사이에 존재하는 의존성을 이해하라
4. 동시성 테스트 방법 & 문제점
🎮 References
- Clean Code
- Effective Java 3/E