본문 바로가기

책/Effective Java

동시성

공유 중인 가변 데이터는 동기화해 사용하라

 - 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.

 - 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