Java Volatile keyword (rpm)

Source:   the Java Volatile keyword

 

  Java's volatile keyword is used to mark a variable "should be stored in main memory." Rather, each reading a volatile variable, it should be read from the main memory, instead of reading from the CPU cache. Each write a volatile variable should be written to main memory, CPU cache rather than just written.

  In fact, since Java 5, volatile keyword in addition to ensuring a volatile variable from outside the main memory read and write, but also provides more protection. I will be explained in later chapters.

 

Variable visibility problems

  Java's volatile keyword ensures variable modified for each thread is visible. This sounds a bit abstract, the following detailed description.

  In a multithreaded application, the thread non-volatile variables during operation, for performance reasons, each thread might copies of variables from main memory to the CPU cache. If your computer has multiple CPU, each thread might run on a different CPU. This means that each thread will variables are likely to copy the respective CPU cache, as shown below:

  For non-volatile variables, the JVM does not guarantee the data read from the main memory to the CPU cache, or to write data in the CPU cache in main memory. This can cause problems in later chapters, I will explain these issues.

  Imagine, if there are two or more threads access a shared object, the shared object contains a counter variable, the following code sample:

public class SharedObject {
    public int counter = 0;
}

  If only modify the thread 1 (increment) counter variable, and the thread 1 and thread 2 reads the counter variable two threads at some point.

  If the counter variable is not declared as volatile, the value of the counter is not guaranteed to be written from the CPU cache back to main memory. That is, the value of the variable counter CPU cache and main memory is not consistent, as shown below:

  This is the "visibility" problem, the thread can not see the latest value of the variable, because the other threads will not write variable values ​​from the CPU cache back to main memory. Modify a thread to another thread is not visible.

 

volatile visibility guarantee

  Java's volatile keyword is designed to solve the problem of variable visibility. The counter variable is declared as volatile, the counter variable at the time of writing, the value of the variable will also be written into main memory. Similarly, when reading the counter value of the variable will be read directly from main memory.

The following code shows that if the counter is declared as volatile:

public class SharedObject {
    public volatile int counter = 0;
}

  A variable declared as volatile, visible to other threads when the tag is written guarantees.

  In the above scenario, a thread (T1) to modify the counter, another thread (T2) of the counter is read (but not modified it), the counter variable is declared as volatile, you can guarantee the write counter variable, on T2 It is visible.

  However, if the T1 and T2 are modified value counter, but the counter is declared as volatile is not enough, there will be more explained later.

 

The volatile guarantee complete visibility

  In fact, the visibility is not only to ensure that volatile so simple for volatile variable itself. Visibility assure compliance with the following rules:

  If thread A is written to a volatile variable, then thread B reads the same variable volatile, visible for all the variables A thread before writing the volatile variable value, after reading the volatile variables also visible in thread B.

  If a thread reads a volatile variable A, then A thread in all the variables will be visible from the main memory is also re-read.

The following example shows a section of code to:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

  update () method writes three variables, which only days variable is volatile.

  The volatile guarantee complete visibility means that when writing days variables, thread all visible variables are also written to main memory. That is, when the variable write days, months and years will also be written to main memory.

The following code reads the years, months, days variables:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

  Please note totalDays () method reads days to total variable variable values. When the variable value is read days, months, and years of values ​​will also read from the main memory. Thus, when reading the sequence illustrated above, can be read to ensure that the latest value of days, months, years of variables.

'' Translator's Note: You can read and write volatile variable will be understood as a trigger refresh operation, writing volatile variables, all variables in the thread will also trigger written into the main memory. While reading a volatile variable, it will also trigger a thread in all the variables read from the main memory again. Therefore, we should try to volatile write operations on the last, and the volatile read on the front, so that we can jointly and severally other variables will also be refreshed. In the above example, update () method of the assignment is on days years, months later, is to ensure years, months can also be written to main memory the last value, if it is placed before the two variables, it will be days written to main memory, the years, months does not. In turn, read totalDays () method will be placed in front of days, just to be able to simultaneously trigger a refresh years, months variable value, if it is put back, the years, months could still read values from the CPU cache, get the latest value rather than from main memory. ''

 

Instruction reordering problem

For performance reasons, JVM, and the program of the CPU is to allow rearranging instructions, instructions as long as can be semantically consistent (after rearrangement). The following code as an example:

int a = 1;
int b = 2;

a++;
b++;

这些指令可以按以下顺序重排,而不改变程序的语义:

int a = 1;
a++;

int b = 2;
b++;

然而,指令重排面临的一个问题就是对volatile变量的处理。还是以前面提到的MyClass类来说明:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦update()变量写了days值,则years、months的最新值也会写入到主存。但是,如果JVM重排了指令,比如按以下方式重排:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

  在days被修改时,months、years的值也会写入到主存,但这时进行写入,months、years并不是新的值(译者注:即在months、years被赋新值之前,就触发了这两个变量值写入主存的操作,自然这两个变量在主存中的值就不是新值)。新的值自然对其他线程是不可见的。指令重排导致了程序语义的改变。

 

Java对此有一个解决方法,我们会在下一章节中说明。

 

Java volatile Happens-Before保证

  为了解决指令重排的问题,Java的volatile关键字在可见性之外,又提供了happends-before保证。happens-before原则如下:

  如果有读写操作发生在写入volatile变量之前,读写其他变量的指令不能重排到写入volatile变量之后。写入一个volatile变量之前的读写操作,对volatile变量是有happens-before保证的。注意,如果是写入volatile之后,有读写其他变量的操作,那么这些操作指令是有可能被重排到写入volatile操作指令之前的。但反之则不成立。即可以把位于写入volatile操作指令之后的其他指令移到写入volatile操作指令之前,而不能把位于写入volatile操作指令之前的其他指令移到写入volatile操作指令之后。

  如果有读写操作发生在读取volatile变量之后,读写其他变量的指令不能重排到读取volatile变量之前。注意,如果是读取volatile之前,有读取其他变量的操作,那么这些操作指令是有可能被重排到读取volatile操作指令之后的。但反之则不成立。即可以把位于读取volatile操作指令之前的指令移到读取volatile操作指令之后,而不能把位于读取volatile操作指令之后的指令移到读取volatile操作指令之前。

 

以上的happens-before原则为volatile关键字的可见性提供了强制保证。

''译者注:这两个原则读起来有些拗口(当然翻译也不足够好),其实就是不管JVM怎么去禁止/允许某些情况下的指令重排,最终就是保证“完整的volatile可见性保证”的那种效果,所以,只要理解了“完整的volatile可见性保证”的效果就足够了 ''

volatile并不总是可行的 

  虽然volatile关键字能保证volatile变量的所有读取都是直接从主存读取,所有写入都是直接写入到主存中,但在一些情形下,仅仅是将变量声明为volatile还是远远不够的。

  就像前面示例所说的,线程1写入共享变量counter的值,将counter声明为volatile已经足够保证线程2总是能获取到最新的值。

  事实上,多个线程都能写入共享的volatile变量,主存中也能存储正确的变量值,然而这有一个前提,变量新值的写入不能依赖于变量的旧值。换句话说,就是一个线程写入一个共享volatile变量值时,不需要先读取变量值,然后以此来计算出新的值。

  如果线程需要先读取一个volatile变量的值,以此来计算出一个新的值,那么volatile变量就不足够保证正确的可见性。(线程间)读写volatile变量的时间间隔很短,这将导致一个竞态条件,多个线程同时读取了volatile变量相同的值,然后以此计算出了新的值,这时各个线程往主存中写回值,则会互相覆盖。

  多个线程对counter变量进行自增操作就是这样的情形,下面的章节会详细说明这种情形。 

  设想一下,如果线程1将共享变量counter的值0读取到它的CPU缓存,然后自增为1,而还没有将新值写回到主存。线程2这时从主存中读取的counter值依然是0,依然放到它自身的CPU缓存中,然后同样将counter值自增为1,同样也还没有将新值写回到主存。如下图所示:

  从实际的情况来看,线程1和线程2现在就是不同步的。共享变量counter正确的值应该是2,但各个线程中CPU缓存的值都是1,而主存中的值依然是0。这是很混乱的。即使线程最终将共享变量counter的值写回到主存,那值也明显是错的。

 

何时使用volatile

  正如我前面所说,如果两个线程同时读写一个共享变量,仅仅使用volatile关键字是不够的。你应该使用 synchronized 来保证读写变量是原子的。(一个线程)读写volatile变量时,不会阻塞(其他)线程进行读写。你必须在关键的地方使用synchronized关键字来解决这个问题。

  In addition to synchronized method, you can also use a number of atomic data types java.util.concurrent package offered to solve this problem. For example, AtomicLong or AtomicReference, or other classes.

  If only one thread of the volatile read and write, while the other thread just read a variable, then, to just read a variable thread is, volatile would be able to guarantee to read the latest value of the variable. If you do not declare a variable as volatile, which can not be guaranteed.

  volatile keyword is valid for 32-bit and 64-bit variables.

 

volatile performance considerations

  Read-write volatile variable causes the variable read from main memory. From main memory to read and write more "costly" than the cache read and write from the CPU. Will also access a volatile variable prohibition instruction rearrangement, rearrangement of the instruction is a performance enhancing technologies. Therefore, you should only need to ensure that in the case of variable visibility only use volatile variables.

 

Guess you like

Origin www.cnblogs.com/myseries/p/11939040.html