[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을 제거해야 함

java.util.concurrent.atomic 을 이용한 락-프리 동기화

private static final AtomicLong nextSerialNumber = new AtomicLong();

public static long generateSerialNumber() {
  return nextSerialNumber.getAndIncrement();
}
  • 성능이 동기화 버전보다 우수
  • 원자성까지 지원

정리

  • 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야 한다.
  • 가변 데이터는 단일 스레드에서만 쓰도록 하자!