[Effective Java 3/e] 아이템 78 - 공유 중인 가변 데이터는 동기화해 사용하라
아이템 78 - 공유 중인 가변 데이터는 동기화해 사용하라
동기화
- synchronized 키워드
- 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장
- 동기화는 배타적 실행뿐만 아니라 스레드 사이의 안정적인 통신에 꼭 필요함
- Thread.stop 은 사용금지
- 데이터 훼손 가능성 있음
적절히 동기화해 스레드가 정상 종료한다
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
- 쓰기와 읽기 모두 동기화 해야 동작을 보장할 수 있다.
- 동기화 비용을 줄일러면 volatile 한정자를 사용할 수 있다.
volatile 필드를 사용해 스레드가 정상 종료한다
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
- volatile 한정자는 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
잘못된 코드 - 동기화가 필요하다
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
- 증가 연산자(++) 사용 시 문제점
- 값을 읽고, 1증가한 값을 저장함 -> nextSerialNumber 두 번 접근
- 두 번째 스레드가 이 두 접근 사이를 비집고 들어와 값을 읽어가면 첫 번째 스레드와 같은 값을 돌려받게 된다.
- 해결법
- generateSerialNumber 메서드에 synchronized 한정자를 붙이고 volatile을 제거해야 함
- 값을 읽고, 1증가한 값을 저장함 -> nextSerialNumber 두 번 접근
java.util.concurrent.atomic 을 이용한 락-프리 동기화
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
- 성능이 동기화 버전보다 우수
- 원자성까지 지원
정리
- 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다.
- 가변 데이터는 단일 스레드에서만 쓰도록 하자!