Home > Java > javaTutorial > Detailed explanation of Java's Volatile keyword

Detailed explanation of Java's Volatile keyword

黄舟
Release: 2017-02-28 10:48:59
Original
1997 people have browsed it

This Java volatile keyword is used to mark a Java variable as "being stored in main memory". More precisely it means that every read of a volatile variable is read from the computer's main memory, rather than from the CPU cache, and that every write to a volatile variable will be written to main memory, Rather than just writing to the CPU cache.

In fact, since Java5, the volatile keyword not only ensures that variables are written to main memory, but also read from main memory. I will explain in the next section.

The guarantee of visibility of Java’s volatile keyword

This Java’s volatile keyword guarantees the visibility of variable changes across threads sex. This one might sound a bit abstract, so let’s elaborate on it.

In a multi-threaded application, threads operate on non-volatile variables. Each thread may copy variables from main memory into the CPU cache when working, for performance reasons. If your computer contains more than one CPU, each thread may run on a different CPU. That means that each thread will copy the variables into the CPU cache of different CPUs. As shown in the figure below:


Using non-volatile variables, there is no guarantee when the JVM will read data from the main memory into the CPU cache, or write data From the CPU cache into main memory. This may cause several problems which are explained in the following sections.

Imagine a scenario where two or more threads access a shared object that contains a counter variable declared, like the following:


public class SharedObject {

    public int counter = 0;

}
Copy after login


Also imagine that only thread 1 increments the counter variable, but thread 1 and thread 2 occasionally read this counter variable.

If the counter variable is not declared volatile, there is no guarantee when the counter variable will be written back to the main memory from the CPU cache. This means that the counter variable in the CPU cache is different from the value in main memory. As shown in the figure below:


The problem that the thread cannot see the latest value of this variable is because it has not been written back to the main memory by other threads , don’t call it a “visibility” issue. Updates from one thread are invisible to other threads.

By declaring the counter variable volatile, all writes to the counter variable will be immediately written back to the main memory. At the same time, all reads of the counter variable will be read directly from main memory. Here is how to declare a counter variable as volatile:


public class SharedObject {

    public volatile int counter = 0;

}
Copy after login


Declare a variable as volatile, so writes to this variable can be guaranteed Visibility to other threads.

This Java volatile keyword ensures the order of precedence and sequence

Since Java5, this volatile keyword not only guarantees the reading and writing of variables from main memory. In fact, the volatile keyword also guarantees this:


  • If thread A writes to a volatile variable, and thread B subsequently reads this variable, Then before writing this volatile variable, all variables are visible to thread A, and they will also be visible to thread B after it has read this volatile variable.

  • Read and write instructions for volatile variables will not be reordered by the JVM (as long as the JVM detects that the program activity from the reordering has not changed, the JVM may reorder the instructions for performance reasons) ). Instructions can be reordered before and after, but volatile keyword reads or writes are not mixed with these instructions. No matter what instructions follow a read or write to a volatile variable, the order of reading or writing is guaranteed.

These statements require deeper explanation.

When a thread writes a volatile variable, then not only the volatile variable itself is written back to the main memory. All other variables changed by this thread before writing this volatile variable will also be written back to main memory. When a thread reads a volatile variable, it also reads all other variables that are written back to main memory along with the volatile variable.

Look at this example:

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

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;
Copy after login


Because before writing this volatile counter, thread A writes the non-volatile nonVolatile variable, and then when thread A writes When this counter (volatile variable) is reached, non-volatile variables are also written back to the main memory.

Because thread B starts to read the volatile variable counter, then the counter variable and the nonVolatile variable will be read by thread B from the main memory to the CPU cache. At this time, thread B will also see the nonVolatile variable written by thread A.

Developers may use this extended visibility guarantee to optimize the visibility of variables between threads. Instead of declaring every variable volatile, only one or a few variables need to be declared volatile. Here is an example:

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;
    }
}
Copy after login


线程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;
Copy after login


注意这个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;
Copy after login


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变量,这个就不能保证。

Performance considerations of the volatile keyword

Reading and writing of volatile variables causes this variable to be read or written to main memory. Reading from or writing to main memory is more expensive than accessing the CPU cache. Accessing volatile variables also prevents instruction reordering, which is a standard performance-increasing technique. Therefore, you should only use volatile variables when you really need strong visibility of the variable.

The above is the detailed explanation of Java’s Volatile keyword. For more related content, please pay attention to the PHP Chinese website (www.php.cn)!


Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template