본문 바로가기

책정리/EfectiveJava

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

동기화란?


  • 여러 스레드가 한개의 자원을 사용할 때 점유한 스레드를 제외 하고  나머지 스레드들은 접근하지 못하도록 베타적인 락을 통해 보호 기능을 사용하는 것을 말함. (Synchronized)
  • 하지만 자바에서는 volatile변수, 명시적 락, 단일 연산 변수(Atomic variable)을 사용하는 경우에도 '동기화'라는 용어를 사용 함.
  •  동기화는 일관성이 깨진 상태를 볼 수 없게 함
  • 스레가드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해줌 
  • long과 double  외에 변수를 읽고 쓰는 동작은 원자적이다.
  • 그렇다고 해서 성능을 높으려고 읽고 쓸때 동기화를 하지 않으면 아주 위험한 발상
  • 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장
  • 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않음

 


public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) throws InterruptedException {
        Thread backgroukndTrehad = new Thread(() -> {
            int i = 0;
            while (!stopRequested) {
                i++;
            }
        });
        backgroukndTrehad.start();

        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

 메인 스레드가 1초 후 stopReqeusted를 ture로 설정하면 반복문을 빠져나올 것처럼 보임

 하지만 영원히 수행 됨

 

 가상 머신은 다음과 같이  끌어올리기 (hoisting)라는 최적화 기법을 수행함.

 

 //원래 코드
 while (!stopRequested)
   i++;
   
 // 최적화한 코드
 if (!stopRequested)
   while(true)
      i++;

 

stopRequested필드를 동기화 해 접근하면 이 문제를 해결할 수 있다.

쓰기 메서드와 읽기 메서드 모두 동기화 했음을 주목 

쓰기와 읽기가 동기화되지 않으면 동작을 보장하지 않음

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 backgroukndTrehad = new Thread(() -> {
            int i = 0;
            while (!stopRequested()) {
                i++;
            }
        });
        backgroukndTrehad.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

 

volatile과 안전 실패(safety failure)

 

 

  • 위 코드에서 stopRequeted 를 volatile으로 선언하면 동기화를 생략해도 됨
  • volatile은  배타적 수행과 상관 없지만 항상 가장 최근에 기록된 값을 일게 됨을 보장
  • volatile은 주의해서 사용해야 함.
private static voliatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
	return nextSerialNumber++;
}

 

  • 일련번호를 생성할 의도로 작성한 코드이고 매번 고유한 값을 반환할 의도로 만들어짐
  • 동기화 하지 않더라고 불변식을 보호할 수 있어 보이지만 동기화 없이 올바르게 동작하지 않음
  • 문제는 증가 연산자
  • 실제로 nextSerialNumber 필드에 두번 접근
  • 먼저 값을 읽고 그런 다음 증가한 새로운 값을 저장
  • 만약 두 번째 스레드가 이 두 접근 사이를 비집고 들어와 값을 읽어가면 첫 번째 스레드와 똑같은 값을 받게됨
  • 이런 오류를 안전 실패라고 함
  • generateSerialNumber메소드에 synchronized를 붙이면 해결

 

 

AtuomicLong


volatile은 동기화의 두 효과 중 통신 쪽만 지원하지만 이 패키지는 원자성(배타적 실행)가지 지원 

 

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
	return nextSerialNum.getAndIncrement();
}

 

 문제들을 피하는 방법


애초에 가변 데이터를 공유하지 않거나 불변 데이터만 공유하거나 아무것도 공유하지 말자

가변 데이터는 단일 스레드에서만 쓰도록 하자

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화 해야함.