Java의 Volatile 키워드 가이드

1. 개요

필요한 동기화가없는 경우 컴파일러, 런타임 또는 프로세서는 모든 종류의 최적화를 적용 할 수 있습니다. 이러한 최적화는 대부분의 경우 유용하지만 때로는 미묘한 문제를 일으킬 수 있습니다.

캐싱 및 재정렬은 동시 컨텍스트에서 우리를 놀라게 할 수있는 최적화 중 하나입니다. Java와 JVM은 메모리 순서를 제어하는 ​​여러 가지 방법을 제공하며 volatile 키워드는 그중 하나입니다.

이 기사에서는 Java 언어에서 자주 오해되는이 기본적이지만 종종 오해되는 개념 인 volatile 키워드에 초점을 맞출 것 입니다. 먼저 기본 컴퓨터 아키텍처가 작동하는 방법에 대한 배경 지식부터 시작한 다음 Java의 메모리 순서에 익숙해집니다.

2. 공유 다중 프로세서 아키텍처

프로세서는 프로그램 명령 실행을 담당합니다. 따라서 RAM에서 프로그램 명령과 필요한 데이터를 모두 검색해야합니다.

CPU는 초당 상당한 수의 명령을 수행 할 수 있으므로 RAM에서 가져 오는 것은 이상적이지 않습니다. 이러한 상황을 개선하기 위해 프로세서는 Out of Order Execution, Branch Prediction, Speculative Execution 및 물론 Caching과 같은 트릭을 사용하고 있습니다.

다음과 같은 메모리 계층 구조가 작동합니다.

서로 다른 코어가 더 많은 명령을 실행하고 더 많은 데이터를 조작함에 따라 캐시를 더 관련성있는 데이터와 명령으로 채 웁니다. 이렇게하면 캐시 일관성 문제가 발생하는 대신 전체 성능이 향상됩니다 .

간단히 말해, 한 스레드가 캐시 된 값을 업데이트 할 때 어떤 일이 발생하는지 두 번 생각해야합니다.

3. 휘발성 사용시기

캐시 일관성에 대해 더 확장하기 위해 Java Concurrency in Practice 책에서 한 가지 예를 빌려 보겠습니다.

public class TaskRunner { private static int number; private static boolean ready; private static class Reader extends Thread { @Override public void run() { while (!ready) { Thread.yield(); } System.out.println(number); } } public static void main(String[] args) { new Reader().start(); number = 42; ready = true; } }

TaskRunner의 클래스는 두 가지 간단한 변수를 유지합니다. 메인 메소드에서, 그것은 거짓 인준비 변수 에서 회전하는 또 다른 스레드를 생성 합니다. 변수가 참이 되면 스레드는 단순히 숫자 변수 를 인쇄합니다 .

많은 사람들은이 프로그램이 짧은 지연 후 단순히 42를 인쇄하기를 기대할 수 있습니다. 그러나 실제로 지연은 훨씬 더 길 수 있습니다. 영원히 멈추거나 0으로 인쇄 될 수도 있습니다!

이러한 이상 현상의 원인은 적절한 메모리 가시성과 재정렬이 부족하기 때문입니다 . 더 자세히 평가 해 봅시다.

3.1. 메모리 가시성

이 간단한 예제에는 두 개의 애플리케이션 스레드, 즉 메인 스레드와 리더 스레드가 있습니다. OS가 두 개의 서로 다른 CPU 코어에서 해당 스레드를 예약하는 시나리오를 상상해 보겠습니다.

  • 메인 스레드는 코어 캐시 에 준비숫자 변수 의 복사본을 가지고 있습니다.
  • 리더 스레드도 사본으로 끝납니다.
  • 주 스레드는 캐시 된 값을 업데이트합니다.

대부분의 최신 프로세서에서는 쓰기 요청이 발행 된 직후에 적용되지 않습니다. 실제로 프로세서는 이러한 쓰기를 특수 쓰기 버퍼에 큐에 넣는 경향이 있습니다. 잠시 후, 그들은 이러한 쓰기를 한꺼번에 메인 메모리에 적용합니다.

모든 것을 말하면 메인 스레드가 준비 변수를 업데이트 할 때 리더 스레드가 무엇을 볼 수 있는지에 대한 보장이 없습니다. 즉, 판독기 스레드는 업데이트 된 값을 즉시 또는 약간의 지연이 있거나 전혀 볼 수 없습니다!

이러한 메모리 가시성은 가시성에 의존하는 프로그램에서 활성 문제를 일으킬 수 있습니다.

3.2. 재정렬

설상가상으로 리더 스레드는 실제 프로그램 순서가 아닌 다른 순서로 이러한 쓰기를 볼 수 있습니다 . 예를 들어, 먼저 숫자 변수를 업데이트하기 때문에 :

public static void main(String[] args) { new Reader().start(); number = 42; ready = true; }

판독기 스레드가 42를 인쇄 할 것으로 예상 할 수 있습니다. 그러나 실제로는 인쇄 된 값으로 0을 볼 수 있습니다!

재정렬은 성능 향상을위한 최적화 기술입니다. 흥미롭게도 다른 구성 요소가이 최적화를 적용 할 수 있습니다.

  • 프로세서는 프로그램 순서 이외의 순서로 쓰기 버퍼를 플러시 할 수 있습니다.
  • 프로세서는 비 순차적 실행 기술을 적용 할 수 있습니다.
  • JIT 컴파일러는 재정렬을 통해 최적화 할 수 있습니다.

3.3. 휘발성 메모리 순서

변수에 대한 업데이트가 다른 스레드에 예측 가능하게 전파되도록하려면 해당 변수에 volatile 한정자를 적용해야 합니다.

public class TaskRunner { private volatile static int number; private volatile static boolean ready; // same as before }

이런 식으로 우리는 휘발성 변수 와 관련된 명령을 재정렬하지 않도록 런타임 및 프로세서와 통신 합니다. 또한 프로세서는 이러한 변수에 대한 업데이트를 즉시 플러시해야 함을 이해합니다.

4. 휘발성 및 스레드 동기화

다중 스레드 응용 프로그램의 경우 일관된 동작을 위해 몇 가지 규칙을 확인해야합니다.

  • 상호 배제 – 한 번에 하나의 스레드 만 중요 섹션을 실행합니다.
  • 가시성 – 데이터 일관성을 유지하기 위해 한 스레드에서 공유 데이터에 대한 변경 사항을 다른 스레드에서 볼 수 있습니다.

동기화 된 메서드 및 블록은 애플리케이션 성능을 희생하면서 위의 두 속성을 모두 제공합니다.

volatile물론 상호 배제를 제공하지 않고도 데이터 변경의 가시성 측면을 보장 있기 때문에 매우 유용한 키워드 입니다. 따라서 여러 스레드가 코드 블록을 병렬로 실행해도 괜찮은 곳에서 유용하지만 가시성 속성을 보장해야합니다.

5. 주문 전 발생

휘발성 변수 의 메모리 가시성 효과 는 휘발성 변수 자체를 넘어 확장 됩니다.

좀 더 구체적으로 말하자면, 스레드 A가 휘발성 변수에 쓴 다음 스레드 B가 동일한 휘발성 변수를 읽는 다고 가정 해 보겠습니다 . 이러한 경우, 기록 용 전에 볼 수 있었던 값 휘발성 판독 후 B로 볼 수 있습니다 변수를 휘발성 변수를 :

기술적으로 말하면 휘발성 필드 에 대한 모든 쓰기 는 동일한 필드를 이후에 읽을 때마다 발생합니다 . 이것은 JMM (Java Memory Model )의 휘발성 변수 규칙입니다.

5.1. 편승

메모리 순서가 발생하기 전에 발생하는 강도 때문에 때때로 다른 휘발성 변수 의 가시성 속성에 편승 할 수 있습니다 . 예를 들어, 우리의 특정 예에서는 ready 변수를 volatile 로 표시하기 만하면 됩니다 .

public class TaskRunner { private static int number; // not volatile private volatile static boolean ready; // same as before }

이전 기록에 아무것도 진정한 받는 준비 변수는 읽은 후 아무것도 볼 준비가 변수. 따라서 number 변수는 ready 변수에 의해 시행되는 메모리 가시성에 피기 백됩니다 . 간단히 말하면 , 그것은하지 비록 휘발성 그것이 전시되고, 변수 휘발성 동작을.

이러한 의미를 사용함으로써 클래스의 변수 중 몇 개만 휘발성으로 정의 하고 가시성 보장을 최적화 할 수 있습니다.

6. 결론

이 튜토리얼에서는 volatile 키워드와 그 기능 에 대해 자세히 살펴보고 Java 5부터 개선 된 사항을 살펴 보았습니다 .

항상 그렇듯이 코드 예제는 GitHub에서 찾을 수 있습니다.