이펙티브 자바 11장: 동시성

Adam·2024년 9월 26일
0

이펙티브 자바

목록 보기
10/10
post-thumbnail

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

synchronized 키워드: 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장

동기화

  1. 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변화
  2. 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못한다
  • 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장
  • 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요

가변 데이터는 단일 스레드에서만 써야 한다

과도한 동기화는 피하라

응답 불가와 안전 실패를 피하려면 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 양도하면 안 된다

동기화 영역에서는 가능 한 일을 적게 하는 것이 좋다.

동기화는 멀티코어 세상인 지금 성능을 급격히 저하 시키고, 합당한 이유가 있을 때만 내부에서 동기화하고, 동기화 여부는 문서화 하는 것이 좋다

스레드보다는 실행자, 태스크, 스트림을 애용하라

실행자는 사용하기 매우 간단하다

// 생성
ExecutorServcice exec = Executors.newSingleThreadExecutor();
//실행
exec.execute(runnable);
//우아한 종료
exec.shutdown();

큐를 둘 이상의 스레드가 처리하게 하고 싶다면 스레드 풀을 생성해 처리하는 것이 좋다

작은 프로그램이나 가벼운 서버라면 Executors.newCachedThreadPool을 사용하는 것이 좋다

무거운 서버라면 스래드 개수를 고정한 Executors.newFixedThreadpool 혹은 직접 통제할 수 있는 ThreadPoolExecutor을 직접 사용하는 것이 좋다

wait와 notify보다는 동시성 유틸리티를 애용하라

wait와 notify는 올바르게 사용하기 어려우니 고수준 동시성 유틸리티를 사용하는 것이 좋다

동시성 컬랙션에서 동시성을 무력화하는 건 불가능하며, 외부에서 락을 추가로 사용하면 오히려 속도가 느려진다

Collections.sychronizedMap보다는 ConcurrentHashMap이 성능이 더 좋기 떄문에 ConcurrentHashMap을 사용하는게 좋다

시간 간격을 잴 때는 항상 System.currentTimeMillis가 아닌 System.nanoTime이 더 정확하니 nanoTime을 사용해야 한다

Wait 메서드를 사용할 때는 반드시 대기 반복문 관용구를 사용하고 반복문 밖에서는 절대로 호출하면 안된다

스레드를 전부 깨우는 notifyAll이 읿반적으로 하나의 스레드를 깨우는 notify보다 좋다

스레드 안전성 수준을 문서화하라

메서드 선언에 synchronized 한정자를 선언할지는 구현 이슈일 뿐 API에 속하지는 않아서 이것만으로 그 메서드가 스레드 안전하다고 믿기 어렵다

멀티스레드 환경에서도 API를 안전하게 사용하게 하려면 클래스가 지원하는 스레드 안전성 수준을 정확히 명시해야 한다.

스레드 안전성이 높은 순위

  1. 불변: 이 클래스의 인스턴스는 상수와 같아서 외부 동기화도 필요 없다
  2. 무조건적 스레드 안전: 이 클래스의 인스턴스는 수정될 수 있으나, 내부에서 충실히 동기화하여 별도의 외부 동기화 없이 동시에 사용해도 안전
  3. 조건부 스레드 안전: 전반적으로는 안전하나 일부 메서드를 사용하려면 외부 동기화가 필요함
  4. 스레드 안전하지 않음: 이 클래스의 인스턴스는 수정될 수 있음. 동시에 사용하려면 각가의 메서드 호출을 클라이언트가 선택한 외부 동기화 메커니즘으로 감싸야 한다 ex)ArrayList, HashMap
  5. 스레드 적대적: 외부 동기화로 감싸더라도 멀티스레드 환경에서 안전하지 않다. 데이터를 아무 동기화 없이 수정

무조건적 스레드 안전 클래스를 작성할 때는 비공개 락 객체를 사용해지 하위 클래스에서 동기화 매커니즘을 깨뜨리는 걸 예방할 수 있다

지연 초기화는 신중히 사용하라

지연 초기화: 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법

//일반 초기화
private final FieldType field = computedFieldValue();

//지연 초기화
private synchronized FieldType getField(){
	if(field == null)
		field = computeFieldValue();
	return field;
}

클래스 혹은 인스턴스 생성 시 초기화 비용은 줄지만 지연 초기화하는 필드에 접근하는 비용은 커진다

자주 호출하면 성능이 오히려 안좋아 질 수 있음

인스턴스 사용 비율이 낮고, 초기화 비용이 작으면 지연 초기화가 좋지만 일반적인 상황에서는 일반 초기화가 좋다

성능 때문에 정적 필드를 지연 초기화해야 한다면 지연 초기화 홀더 클래스 관용구를 사용하고 이중검사 관용구를 사용해야 한다.

프로그램의 동작을 스레드 스케줄러에 기대지 말라

정확성이나 성능이 스레드 스케줄러에 따라 달라지는 프로그램이라면 다른 플랫폼에 이식하기 어렵다

스레드는 당장 처리해야 할 작업이 없다면 실행돼서는 안 된다

Thread.yield와 스레드 우선순위에 의존해서는 안된다

스레드 우선순위는 이미 잘 동작하는 프로그램의 서비스 품질을 높이기 위해 드물게 쓰고, 프로그램을 고치는 용도로는 써서 안된다

profile
Keep going하는 개발자

0개의 댓글