공유 중인 가변 데이터는 동기화해 사용하라
- 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
- Thread.stop은 사용하지 말자.
- 가변 데이터는 단일 스레드에서만 쓰도록 하자.
- 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.
- 동기화하지 않으면 한스레드가 수행한 변경을 다른 스레드가 보지 못할 수도 있다.
- 공유되는 가변 데이터를 동기화하는데 실패하면 응답 불가 상태에 빠지거나 안전 실패로 이어질 수 있다.(디버깅 난이도가 가장 높은 문제에 속한다.)
- 배타적 실행은 필요 없고 스레드끼리의 통신만 필요하다면 volatile 한정자만으로 동기화할 수 있다.
과도한 동기화는 피하라
- 응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안된다.
- 동기화 영역에서는 가능한 일을 적게 해야 한다.
- 가변 클래스를 설계할 때는 스스로 동기화해야 할지 고민하자.
- 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부를 문서에 정확히 밝히자.
스레드보다는 실행자, 태스크, 스트림을 애용하라
- 실행자 : ExcutorService exec = Excutors.newSingleThreadExecutor(); or Excutors.newFixedThreadPool(5);
- 태스크 : exec.execute(runnable);
- 종료 : exec.shutdown();
wait과 notify보다는 동시성 유틸리티를 애용하라
- wait와 notify는 올바르게 사용하기가 까다로우니 고수준 동시성 유틸리티를 사용하자. (코드를 새로 작성한다면 wait와 notify를 쓸 이유가 거의 없다.)
- 동시성 컬렉션에서 동시성을 무력화하는 건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다.
- Collections.synchronizedMap보다는 ConcurrentHashMap을 사용하는 게 좋다.
- 시간 간격을 잴 때는 System.currentTimeMillis가 아닌 System.nanoTime을 사용하자.
- wait 메서드를 사용할 때는 반드시 대기 반복문 관용구를 사용하라. 반복문 밖에서는 절대로 호출하지 말자.
synchronized (obj) {
while(...) { // 조건이 충족되지 않을 경우
obj.wait(); // 락을 놓고, 깨어나면 다시 잡는다.
}
... // 조건이 충족됐을 때의 동작을 수행
}
- 일반적으로 notufy보다는 notifyAll을 사용하자. 혹시라도 notify를 사용한다면 응답 불가 상태에 빠지지 않도록 각별히 주의하자.
스레드 안전성 수준을 문서화하라
- 메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일 뿐 API에 속하지 않는다.
- 멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명시해야 한다.
- 스레드 안전성이 높은 순서
불변 : String, Long, BigInteger
무조건적 스레드 안전 : AtomicLong, CucurrentHashMap
조건부 스레드 안전 : Collections.synchronized 래퍼 메서드가 반환한 컬렉션
스레드 안전하지 않음 : ArrayList, HashMap
스레드 적대적 : 정적 변수를 동기화 없이 수정하는 경우
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
- 무조건적 스레드 안전 클래스를 작성할 때는 synchronized 메서드가 아닌 비공개 락 객체를 사용하자. 락 객체는 항상 final로 선언하자.
지연 초기화는 신중히 사용하라
- 대부분의 상황에서 일반적인 초기화가 지연 초기화보다 낫다.
private final FieldType field = computeFieldValue();
- 지연 초기화가 초기화 순환성을 깨뜨릴 것 같으면 synchronized를 단 접근자를 사용하자.
private FieldType field;
private Synchronized FieldType getField() {
if (field == null) {
field = computeFieldValue();
}
return field;
}
- 성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하자.
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
private static FieldType getField() {
return FieldHolder.field;
}
- 성능 때문에 인스턴스 필드를 지연 초기화해야 한다면 이중검사 관용구를 사용하라.
private volatile FieldType field;
private FieldType getField() {
FieldType result = field;
if (result != null) { // 첫 번째 검사 (락 사용 안함)
return result;
}
synchronized(this) {
if (result == null) { // 두 번째 검사 (락 사용)
field = computeFieldValue();
}
return field;
}
}
- 반복해서 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다.
private volatile FieldType field;
private FieldType getField() {
if (result == null) { // 두 번째 검사 (락 사용)
field = result = computeFieldValue();
}
return result;
}
프로그램의 동작을 스레드 스케줄러에 기대지 말라
- 정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다.
- 스레드는 당장 처리해야 할 작업이 없다면 실행돼서는 안 된다.
- 특정 스레드가 다른 스레드들과 비교해 CPU 시간을 충분히 얻지 못해서 간신히 돌아가는 프로그램을 보더라도 Thread.yield를 사용하지 말자.
- 스레드 우선순위를 조정하는 것도 간신히 돌아가는 프로그램을 '고치는 용도'로 사용해서는 안 된다.
'책 > Effective Java' 카테고리의 다른 글
직렬화 (0) | 2020.04.01 |
---|---|
예외 (0) | 2020.03.27 |
일반적인 프로그래밍 원칙 (0) | 2020.03.26 |
메서드 (0) | 2020.03.24 |
람다와 스트림 (0) | 2020.03.20 |