요약

동기화는 배타적 실행 뿐만 아니라 스레드 간 안정적인 통신을 보장하기 때문에 공유 중인 가변 데이터는 동기화해 사용해야 한다.

그렇지 않으면 응답 불가 상태에 빠지거나 안전 실패(프로그램이 잘못된 결과를 계산해내는 오류)로 이어질 수 있다.

 

동기화의 두 가지 기능

  1. 배타적 실행

배타적 실행은 한 스레드가 객체의 상태를 변경하고 있을 때, 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는 것을 의미한다. 따라서, 동기화를 사용하면 항상 일관적인 객체 상태를 보장할 수 있다.

  1. 스레드 사이의 안정적 통신 보장

동기화를 사용하면 하나의 스레드에서 수행한 작업의 결과를 다른 스레드가 접근해 상태를 확인할 수 있다.

 

원자적 데이터

long, double 외의 type 데이터를 읽고 쓰는 동작은 원자적

→ 여러 스레드가 동기화 없이 수정하는 중에도 다른 스레드가 저장한 값을 읽어올 수 있다.

→ BUT, 원자적 데이터로 만들고 동기화를 하지 말아야겠다는 생각 NO

→ 자바에서는 스레드가 필드를 읽을 때 항상 수정이 완전하게 반영된 값을 얻지만, 이는 다른 이 값을 다른 스레드가 볼 수 있는지 보장하지는 못한다(자바 메모리 모델이 원인)

실패 예시

  • 로직
    • stopRequested = false로 초기화
    • 첫번째 스레드가 stopRequested를 폴링하다가 stopRequested == true 일 때 loop 멈춤
    • 다른 스레드에서 스레드를 멈추려고 할 때 stopRequest = true
  • 예상 : 1초만에 실행 종료 예상
  • 실제 : 무한 루프
public class StopThread {
    private static boolean stopRequested;

    // 동기화를 사용하지 않아 무한루프를 도는 코드
    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested) { // background 스레드
                i++;
            }
        });
        backgroundThread.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true; // 메인 스레드 
    }
}

원인

  • 동기화를 하지 않았기 때문 → 안정적 통신을 보장한다
  • 동기화를 하지 않아 main 스레드가 수정한 값을 background 스레드가 언제쯤에 보게 될지 보증할 수 없다
  • 동기화가 빠지면 VM이 다음과 같이 최적화를 수행할 수도 있다
    • 이는 OpenJDK 서버 VM이 실제로 적용하는 끌어올리기(hosting)라는 최적화 기법으로, 그 결과 프로그램은 응답 불가(liveness failure) 상태가 되어 더이상 진전이 없다
  • // 원래 코드 while (!stopRequested){ i++; } // 최적화한 코드 if (!stopRequested) { while(true) { i++; } }

해결

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

위 코드의 stopRequested 필드를 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++;
}
  • 의도
    • 2^32을 넘지 않을 경우, 매번 고유한 값 반환하는 것
  • 문제
    • nextSerialNumber가 int 타입으로 원자적으로 접근할 수 있어서 동기화하지 않아도 불변식을 보호할 수 있어 보이지만, 실제로는 올바르게 동작하지 않는다
  • 원인 - 증가 연산자(++)
    • 이 연산자는 nextSerialNumber에 두 번 접근하기 때문에 문제가 발생한다
    • 만약 두번째 스레드가 첫 번째와 두 번째 사이에 접근한다면, +1된 값이 아닌 첫번째 스레드와 똑같은 값을 돌려받게 되는 문제가 발생한다
  • 해결 방법
    1. generateSerialNumber()에 synchronized 설정하기
      1. 여러 스레드가 동시에 호출해도 서로 간섭하지 않으며, 이전 호출이 변경한 값을 읽을 수 있게됨
      2. 이때 nextSerialNumber 앞에 volatile은 제거해야 한다
      3. ➕ int 타입 → long 타입 || nextSerialNumber가 최댓값 도달 시 예외 던지기
      4. ➕ java.util.concurrent.atomic 패키지 클래스(AtomicLong) 사용하기
      • AtomicLong을 활용한 최적화 코드
      • public class StopThreadAtomic { private static final AtomicLong nextSerialNum = new AtomicLong(); public static long generateSerialNumber() { return nextSerialNum.getAndIncrement(); } }

 

애초부터 동기화 문제를 피하는 방법

  1. 가변 데이터는 단일 스레드에서만 쓰도록 하자
  2. 사용하려는 프레임워크와 라이브러리를 깊게 이해하자
  3. 필요한 부분만 부분 동기화 하자
    1. 한 스레드가 데이터를 다 수정한 후, 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화하는 것
    2. 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드를 동기화 없이 자유롭게 값을 읽어갈 수 있다
    3. 이를 사실상 불변(effectively immutable)이라고 한다
    4. 이런 사실상 불변 객체를 다른 스레드에게 전달하는 것을 안전 발행(safe publication)이라 한다

 

참고

자바 메모리 모델 이해하기

+ Recent posts