>  기사  >  Java  >  Java의 Volatile 키워드에 대한 자세한 설명

Java의 Volatile 키워드에 대한 자세한 설명

黄舟
黄舟원래의
2017-02-28 10:48:591960검색

이 Java 휘발성 키워드는 Java 변수를 "메인 메모리에 저장됨"으로 표시하는 데 사용됩니다. 보다 정확하게는 휘발성 변수에 대한 모든 읽기는 CPU 캐시가 아닌 컴퓨터의 주 메모리에서 읽혀지며, 휘발성 변수에 대한 모든 쓰기는 단순히 CPU 캐시에 쓰는 것이 아니라 주 메모리에 기록된다는 의미입니다.

사실 Java5부터 휘발성 키워드는 변수가 메인 메모리에 기록되는 것을 보장할 뿐만 아니라 메인 메모리에서 읽는 것도 보장합니다. 다음 섹션에서 설명하겠습니다.

Java의 휘발성 키워드 가시성 보장

이 Java 휘발성 키워드는 스레드 간 변수 변경의 가시성을 보장합니다. 다소 추상적으로 들릴 수도 있으므로 자세히 설명하겠습니다.

멀티 스레드 애플리케이션에서 스레드는 비휘발성 변수에서 작동합니다. 각 스레드는 성능상의 이유로 작업 시 주 메모리에서 CPU 캐시로 변수를 복사할 수 있습니다. 컴퓨터에 두 개 이상의 CPU가 있는 경우 각 스레드는 서로 다른 CPU에서 실행될 수 있습니다. 즉, 각 스레드는 변수를 다른 CPU의 CPU 캐시에 복사합니다. 아래 그림과 같이


비휘발성 변수를 사용하면 JVM이 주 메모리에서 CPU 캐시로 데이터를 읽어올 것이라는 보장이 없습니다. CPU 캐시에서 메인 메모리로 데이터를 씁니다. 이로 인해 다음 섹션에서 설명하는 여러 가지 문제가 발생할 수 있습니다.

두 개 이상의 스레드가 다음과 같이 선언된 카운터 변수가 포함된 공유 객체에 액세스하는 시나리오를 상상해 보세요.


public class SharedObject {

    public int counter = 0;

}


또한 스레드 1만 카운터 변수를 증가시키지만 스레드 1과 스레드 2는 때때로 이 카운터 변수를 읽는다고 상상해 보세요.

카운터 변수가 휘발성으로 선언되지 않은 경우 카운터 변수가 CPU 캐시에서 메인 메모리로 다시 기록될 것이라는 보장이 없습니다. 이는 CPU 캐시의 카운터 변수가 메인 메모리의 값과 다르다는 것을 의미합니다. 아래 그림과 같이


스레드가 이 변수의 최신 값을 볼 수 없는 문제는 메인 메모리에 다시 쓰여지지 않았기 때문입니다. 다른 스레드에서는 이를 '가시성' 문제라고 부르지 마세요. 한 스레드의 업데이트는 다른 스레드에 표시되지 않습니다.

카운터 변수를 휘발성으로 선언하면 카운터 변수에 대한 모든 쓰기가 즉시 메인 메모리에 다시 기록됩니다. 동시에 카운터 변수의 모든 읽기는 주 메모리에서 직접 읽혀집니다. 카운터 변수를 휘발성으로 선언하는 방법은 다음과 같습니다.


public class SharedObject {

    public volatile int counter = 0;

}


변수를 휘발성으로 선언하면 이 변수에 대한 쓰기가 보장됩니다. 다른 스레드에 대한 가시성.

이 Java 휘발성 키워드는 순방향 및 역방향 순서를 보장합니다.

Java5부터 이 휘발성 키워드는 변수를 메인 메모리에서 읽고 쓰는 것만 보장하는 것이 아닙니다. 실제로 휘발성 키워드도 다음을 보장합니다.


  • 스레드 A가 휘발성 변수에 쓰고 이후에 스레드 B가 이 변수를 읽는 경우, 이 휘발성 변수를 작성하면 모든 변수는 스레드 A에서 볼 수 있으며 이 휘발성 변수를 읽은 후에는 스레드 B에서도 볼 수 있습니다.

  • 휘발성 변수를 읽고 쓰기 위한 지침은 JVM에 의해 재정렬되지 않습니다(JVM이 재정렬로 인한 프로그램 활동이 변경되지 않았음을 감지하는 한 JVM은 지침을 재정렬할 수 있음). 성능상의 이유로) ). 명령어는 전후에 다시 정렬될 수 있지만 휘발성 키워드 읽기 또는 쓰기는 이러한 명령어와 혼합되지 않습니다. 휘발성 변수에 대한 읽기 또는 쓰기 이후에 어떤 명령어가 나오더라도 읽기 또는 쓰기 순서는 보장됩니다.

이 표현들은 좀 더 깊은 설명이 필요합니다.

스레드가 휘발성 변수를 쓰면 휘발성 변수 자체만 메인 메모리에 다시 쓰여지는 것이 아닙니다. 이 휘발성 변수를 쓰기 전에 이 스레드에 의해 변경된 다른 모든 변수도 주 메모리에 다시 기록됩니다. 스레드가 휘발성 변수를 읽을 때 휘발성 변수와 함께 주 메모리에 다시 기록되는 다른 모든 변수도 읽습니다.

이 예를 보세요:

Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;


이 휘발성 카운터를 쓰기 전에 스레드 A는 비휘발성 nonVolatile 변수를 쓴 다음 스레드 A가 쓸 때 이 카운터(휘발성 변수)에 도달하면 비휘발성 변수도 주 메모리에 다시 기록됩니다.

스레드 B가 휘발성 변수 카운터를 읽기 시작하므로 스레드 B가 메인 메모리에서 CPU 캐시로 카운터 변수와 비휘발성 변수를 읽습니다. 이때 스레드 B는 스레드 A가 작성한 비휘발성 변수도 볼 수 있습니다.

개발자는 이 확장된 가시성 보장을 사용하여 스레드 간 변수의 가시성을 최적화할 수 있습니다. 모든 변수를 휘발성으로 선언하는 대신 하나 또는 몇 개의 변수만 휘발성으로 선언하면 됩니다. 예는 다음과 같습니다.

public class Exchanger {

    private Object   object       = null;
    private volatile hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}


线程A可能会通过不断的调用put方法设置对象。线程B可能会通过不断的调用take方法获取这个对象。这个类可以工作的很好通过使用一个volatile变量(没有使用synchronized锁),只要只是线程A调用put方法,线程B调用take方法。

然而,JVM可能重排序Java指令去优化性能,如果JVM可以做这个没有改变这个重排序的指令。如果JVM改变了put方法和take方法内部的读和写的顺序将会发生什么呢?如果put方法真的像下面这样执行:

while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;


注意这个volatile变量的写是在新的对象被真实赋值之前执行的。对于JVM这个可能看起来是完全正确的。这两个写的执行的值不会互相依赖。

然而,重排序这个执行的执行将会危害object变量的可见性。首先,线程B可能在线程A确定的写一个新的值给object变量之前看到hasNewObject这个值设为true了。第二,现在甚至不能保证对于object的新的值是否会写回到主内存中。

为了阻止上面所说的那种场景发生,这个volatile关键字提供了一个“发生前保证”。保证volatile变量的读和写指令执行前不会发生重排序。指令前和后是可以重排序的,但是这个volatile关键字的读和写指令是不能发生重排序的。

看这个例子:

sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;


JVM可能会重排序前面的三个指令,只要他们中的所有在volatile写执行发生前(他们必须在volatile写指令发生前执行)。

类似的,JVM可能重排序最后3个指令,只要volatile写指令在他们之前发生。最后三个指令在volatile写指令之前都不会被重排序。

那个基本上就是Java的volatile保证先行发生的含义了。

volatile关键字不总是足够的

甚至如果volatile关键字保证了volatile变量的所有读取都是从主内存中读取,以及所有的写也是直接写入到主内存中,但是这里仍然有些场景声明volatile是不够的。

在更早解释的场景中,只有线程1写这个共享的counter变量,声明这个counter变量为volatile是足够确保线程2总是看到最新写的值。

事实上,如果在写这个变量的新的值不依赖它之前的值得情况下,甚至多个线程写这个共享的volatile变量,仍然有正确的值存储在主内存中。换句话说,如果一个线程写一个值到这个共享的volatile变量值中首先不需要读取它的值去计算它的下一个值。

如果一个线程需要首先去读取这个volatile变量的值,并且建立在这个值的基础上去生成一个新的值,那么这个volatile变量对于保证正确的可见性就不够了。在读这个volatile变量和写新的值之间的短时间间隔,出现了一个竞态条件,在这里多个线程可能会读取到volatile变量的相同的值生成一个新的值,并且当写回到主内存中的时候,会互相覆盖彼此的值。

多个线程增加相同的值得这个场景,正好一个volatile变量不够的。下面的部分将会详细解析这个场景。

想象下,如果线程1读取值为0的共享变量counter进入到CPU缓存中,增加1并且没有把改变的值写回到主内存中。线程2读取相同的counter变量从主内存中进入到CPU缓存中,这个值仍然为0。线程2也是加1,并且也没有写入到主内存中。这个场景如下图所示:


线程1和线程2现在是不同步的。这个共享变量的真实值应该是2,但是每一个线程在他们的CPU缓存中都为1,并且在主内存中的值仍然是0.它是混乱的。甚至如果线程最后写他们的值进入主内存中,这个值是错误的。

什么时候volatile是足够的

正如我前面提到的,如果两个线程都在读和写一个共享的变量,然后使用volatile关键字是不够的。你需要使用一个synchronized在这种场景去保证这个变量的读和写是原子性的。读或者写一个volatile变量不会堵塞正在读或者写的线程。因为这个发生,你必须使用synchronized关键字在临界区域周围。

作为一个synchronized锁可选择的,你也可以使用在java.util.concurrent包中的许多原子数据类型中的一个。例如,这个AtomicLong或者AtomicReference或者是其他中的一个。

假如只有一个线程读和写这个volatile变量的值,其他的线程只是读取这个变量,然后读的这个线程就会保证看到最新的值了。不使用这个volatile变量,这个就不能保证。

휘발성 키워드의 성능 고려 사항

휘발성 변수를 읽고 쓰면 이 변수가 주 메모리에 읽혀지거나 쓰여집니다. 주 메모리에서 읽거나 쓰는 것은 CPU 캐시에 액세스하는 것보다 비용이 더 많이 듭니다. 휘발성 변수에 액세스하면 표준 성능 향상 기술인 명령어 재정렬도 방지됩니다. 따라서 변수에 대한 강력한 가시성이 정말로 필요한 경우에만 휘발성 변수를 사용해야 합니다.

위 내용은 Java의 Volatile 키워드에 대한 자세한 설명입니다. 더 많은 관련 내용은 PHP 중국어 홈페이지(www.php.cn)를 참고해주세요!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.