규칙 66 변경 가능 공유 데이터에 대한 접근은 동기화하라
- synchronized 키워드는 특정 메서드나 코드 블록을 한 번에 한 스레드만 시용하도록 보장한다.
- 동기화 없이는 한 스레드가 만든 변화를 다른 스레드가 확인할 수 없다.
- 동기화는 스레드가 일관성이 깨진 객체를 관측할 수 없도록 할 뿐 아니라, 동기화 메서드나 동기화 블록에 진입한 스레드가 동일한 락의 보호 아래 이루어진 모든 변경(modification)의 영향을 관측할 수 있도록 보장한다.
- 언어 명세상으로는 필드에서 읽어낸 값은 임의의 값이 될 수 없다고 되어 있으나, 그렇다고 어떤 스레드가 기록한 값을 반드시 다른 스레드가 보게 되리라는 보장은 없다.
- synchronized 키워드는 특정 메서드나 코드 블록을 한 번에 한 스레드만 사용 하도록 보장한다.
- 상호배제성뿐 아니라 스레드 간의 안정적 통신을 위해서도 동기화는 반드시 필요하다.
- 메모리 모델은 한 스레드가 만든 변화를 다른 스레드가 볼 수 있게 되는 시점과, 그 절차를 규정한다.
- Thread.stop은 절대로 이용하지 마라.
- 읽기 연산과 쓰기 연산에 전부 적용하지 않으면 동기화는 아무런 효과도 없다.
- volatle이 상호 배제성을 실현하진 않지만, 어떤 스레드건 가장 최근에 기록된 값을 읽도록 보장한다.
규칙 67 과도한 동기화는 피하라
- 생존 오류나 안전 오류를 피하고 싶으면, 동기화 메서드나 블록 안에서 클라이언트에게 프로그램 제어 흐름(control)을 넘기지 마라.
- 동기화가 적용된 영역 안에서는 재정의(override) 가능 메서드나 클라이언트가 제공한 함수 객체(function object) 메서드를 호출하지 말라
- 불가해(alien) 메서드 ?
- 명심해야 할 것은, 동기화 영역 안에서 수행되는 작업의 양을 기능한 한 줄여야 한다는 것이다.
- 락 분할(lock splitting), 락 스트라이핑(striping), 비봉쇄형 병행성 제어(nonblocking concurrency control)
규칙 68 스레드보다는 실행자와 태스크를 이용하라
- 릴리스 1.5부터 자바 플랫폼에는 java.util.concurrent가 추가되었다. 이 패키지에는 실행자 프레임워크(Executor Framework)라는 것이 들어 있는데, 유연성이 높은 인터페이스 기반 태스크(task) 실행 프레임워크다.
- 작은 프로그램이거나 부하가 크지 않은 서버를 만들 때는 보통 Executors.newCachedThreadPool이 좋다.
- 부하가 심한 환경에 들어갈 서버를 만들 때는 Executors.newFixedThreadPool을 이용해서 스레드 개수가 고정된 풀을 만들거나, 최대한 많은 부분을 직접 제어하기 위해 ThreadPoolExecutor 클래스를 사용하는 것이 좋다.
- 태스크에는 두 가지 종류
- Runnable, Callable
- 실행자 프레임워크는 태스크를 실행하는 부분을 담당
규칙 69 wait나 notify 대신 병행성 유틸리티를 이용하라
- wait와 notify를 정확하게 사용하는 것이 어렵기 때문에, 이 고수준 유틸리티들을 반드시 이용해야 한다.
- Java.util.concurrent
- 실행자 프레임워크
- 병행 컬렉션
- 동기자
- 컬렉션 외부에서 병행성을 처리하는 것은 불가능하다. 락을 걸어봐야 아무 효과가 없을 뿐 아니라 프로그램만 느려진다.
- Collections.synchronizedMap이나 HashTable 대신 ConcurrentHashMap을 사용하도록 하자.
- 컬렉션 인터페이스 기운데 몇몇은 봉쇄 연산(blocking operation)이 기능하도록 확장되었다.
- 동기자(synchronizer)는 스레드들이 서로를 기다릴 수 있도록 하여, 상호협력이 가능하게 한다.
- 카운트다운 래치(countdown latch)는 일회성 배리어(barrier)로서 하나 이상의 스레드가 작업을 마칠 때까지 다른 여러 스레드가 대기할 수 있도록 한다.
- 특정 구간의 실행시간을 쟬 때는 System.currentTimeMillis 대신 System.nanoTime을 사용해야 한다.
- wait 메서드를 호출할 때는 반드시 이 대기 순환문(wait loop) 숙어대로 하자.
- 새로 만드는 프로그램에 wait나 notify를 사용할 이유는 거의 없다.
규칙 70 스레드 안전성에 대해 문서로 남겨라
- synchronized 키워드는 메서드의 구현 상세(implementation detail)에 해당하는 정보이며, 공개 API의 일부가 아니다.
- 병렬적으로 사용해도 안전한 클래스가 되려면, 어떤 수준의 스레드 안전성을 제공하는 클래스인지 문서에 명확하게 남겨야 한다.
- 스레드 안전성을 그 수준별로 요약
- 변경 불가능(immutable)
- 무조건적 스레드 안전성(unconditionally thread-safe)
- 조건부 스레드 안전성(Conditionally thread-safe)
- 스레드 안전성 없음
- 다중 스레드에 적대적(thread-hostile)
규칙 71 초기화 지연은 신중하게 하라
- 대부분의 경우, 지연된 초기회를 하느니 일반 초기회를 하는 편이 낫다.
- 초기화 순환성(initialization circularity) 문제를 해소하기 위해서 초기화를 지연시키는 경우에는 동기화된 접근자(synchronized accessor)를 사용하라.
- 성능 문제 때문에 정적필드 초기화를 지연시키고 싶올 때는 초기화 지연 담당 클래스(lazy initialization holder class) 숙어를 적용하라
- 성능 문제 때문에 객체 필드 초기화를 지연시키고 싶다면 이중 검사(double-check) 숙어를 사용하라.
규칙 72 스레드 스케줄러에 의존하지 마라
- 정확성을 보장하거나 성능을 높이기 위해 스레드 스케줄러에 의존하는 프로그램은 이식성이 떨어진다(non-portable).
- 스레드는 필요한 일을 하고 있지 않을 때는 실행 중이어서는 안 된다.
- Thread.yield를 호출해서 문제를 해결하려고는 하지 마라.
- Thread.yield에는 테스트 가능한 의미(semantic)가 없다.
- 스레드 우선순위는 자바 플랫폼에서 가장 이식성이 낮은 부분 가운데 하나다.
규칙 73 스레드 그룹은 피하라
- 스레드 그룹은 이제 폐기된 추상화 단위다.
- 스레드를 논리적인 그룹으로 나누는 클래스를 만들어야 한다면, 스레드 풀 실행자(thread pool executor)를 이용하는 것이 바람직할 것이다.