본문 바로가기

카테고리 없음

[아이템 78] 공유 중인 가변 데이터는 동기화해서 사용해라

동기화의 기능

  1. 배타적 실행
  • 한 스레드가 변경 중일 때 다른 스레드는 접근할 수 없다.
  • 즉, 일관된 상태를 보장한다.
  1. 안정적 통신
  • 이전 수정의 최종 결과를 보게해줌
  • long, double 같은 원자적 타입은 배타적 실행은 보장하지만 안정적 통신까지는 보장하지 않는다. 따라서 동기화가 필요하다.
  • 자바의 메모리 모델 때문

 

 

가변데이터와 동기화

공유 중인 가변데이터의 동기화에 실패하면 매우 처참할 수 있다

 

ex) Thread.stop은 사용하지말자

 

그럼, 다른 스레드를 멈추게하고 싶다면?

boolean 필드 + polling 을 통해 스스로 멈추게하자.

 

주의할점

그럼 원자적 자원에 접근하는 코드는 동기화를 할 필요가 없을까?

답은 X 이다. 위에서 언급한대로 동기화는 안정적 통신도 지원하는데, 아래와 같은 코드는 그것을 보장할 수 없다.

 

package Chap11_Concurrency.item78;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

// 코드 78-1 잘못된 코드 - 이 프로그램은 얼마나 오래 실행될까? (415쪽)
public class StopThread {
    private static 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;
    }
}


 

이 코드는 JVM hoisting 기법에의해 아래처럼 최적화되고, 결과적으로 멈추지 않는다.

CPU Cache: 이와 같은 변수는 CPU cache 에 저장되고 읽어오게되는데 멀티스레드 환경에서 이는 문제가 될 수 있다

hosting 기법: JVM 의 JIT 컴파일러는 코드의 최적화를 담당하는데, loop문의 변수 hoisting(끌어올리기) 기법도 그의 한 종류이다

 public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
          if(!stopRequest){
             while(true){
            }
          }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1L);
        stopRequested = true; // 1초 후 Thread가 for문 수행을 멈추기를 기대한다. 
}

 

 

이 문제는 아래와 같이 synchronized 키워드를 작성하면 해결할 수 있다.

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();
    }
}

 

집중해서 보아야할 점은 쓰기메서드와 읽기 메서드 둘 다 동기화 처리 했다는 점이다.

이곳에서 synchronized 키워드는 "통신"을 목적으로 사용되었기 때문에 쓰기, 읽기 작업 모두에 처리해줘야한다

 

 

또는, 필드에 volatile 한정자를 추가할 수 있다.

volatile: 변수를 Main Memory에서 읽어옴을 보장한다. 즉, 캐시에서 읽어오는 것이 아니기 때문에 가장 최근 변경사항을 가져올 수 있다

private static volatile boolean stopRequested;

 

 

항상 volatile로 해결할 수 있는 것은아니다.

private static volatile int nextSerialNumber= 0;

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

위 코드는 한번의 연산을 수행하는 것같지만 사실 두번 접근한다.

따라서 그 사이를 다른 스레드가 비집고 들어가면 올바르지 않은 결과를 받을 수 있는데, 이를 안전실패라고 한다.

즉, volatile은 통신은 해결할 수 있지만, 배타적 수행은 보장하지 못한다.

 

 

java.util.concurrent.atomic 패키지에는 이 문제들을 해결해주는 다양한 클래스가 정의되어있다.

image

 

 

 

근본적으로 웬만하면 가변 데이터를 공유하지말자. 또는 단일 스레드에서만 사용하도록하자.

불가피하게 사용해야한다면 꼭 문서에 남기도록하자.