The Javavolatilekeyword is used to mark a Java variable as "being stored in main memory". More precisely that means, that every read of a volatile variable will be read from the computer's main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache.

Actually, since Java 5 thevolatilekeyword guarantees more than just that volatile variables are written to and read from main memory. I will explain that in the following sections.

The Java volatile Visibility Guarantee

The Javavolatilekeyword guarantees visibility of changes to variables across threads. This may sound a bit abstract, so let me elaborate.

In a multithreaded application where the threads operate on non-volatile variables, each thread may copy variables from main memory into a CPU cache while working on them, for performance reasons. If your computer contains more than one CPU, each thread may run on a different CPU. That means, that each thread may copy the variables into the CPU cache of different CPUs. This is illustrated here:

With non-volatile variables there are no guarantees about when the Java Virtual Machine (JVM) reads data from main memory into CPU caches, or writes data from CPU caches to main memory. This can cause several problems which I will explain in the following sections.

Imagine a situation in which two or more threads have access to a shared object which contains a counter variable declared like this:

public class SharedObject {

    public int counter = 0;

}

Imagine too, that only Thread 1 increments thecountervariable, but both Thread 1 and Thread 2 may read thecountervariable from time to time.

If thecountervariable is not declaredvolatilethere is no guarantee about when the value of thecountervariable is written from the CPU cache back to main memory. This means, that thecountervariable value in the CPU cache may not be the same as in main memory. This situation is illustrated here:

The problem with threads not seeing the latest value of a variable because it has not yet been written back to main memory by another thread, is called a "visibility" problem. The updates of one thread are not visible to other threads.

By declaring thecountervariablevolatileall writes to thecountervariable will be written back to main memory immediately. Also, all reads of thecountervariable will be read directly from main memory. Here is how thevolatiledeclaration of thecountervariable looks:

public class SharedObject {

    public volatile int counter = 0;

}

Declaring a variablevolatilethus guarantees the visibility for other threads of writes to that variable.

Still a Race Condition

Please notice, that the above example counter is just an example I used to explain how the Javavolatilekeyword affects when values are read and written from and to main memory. However, simply declaring thecountervariablevolatileis not enough to create a fully thread safe shared counter. Let me explain why:

When two threads first read, test and then write a new value based on an older value to a variable, even if the variable is declaredvolatileyou will still have a race condition. Imagine this situation where two threads named T1 and T2 are reading, testing and writing to a sharedvolatilevariable:

T1: Read counter variable - value is 6
T2: Read counter variable - value is 6

T1: if counter < 100 add 1 to counter
    ->so adds 1 to counter which is now 7 in T1
    ->counter is immediately written back to main memory

T2: if counter < 100 add 1 to counter
    -> so adds 1 to counter which is now 7 in T2
    -> counter is immediately written back to main memory

Value of counter in main memory is now 7,
but should have been 8

Notice how the two threads T1 and T2 read the sharedcountervariable directly from main memory, and write the updated values directly back to main memory, and yet because the read-test-write operation is not carried out as a single atomic operation, both threads read the pre-incremented value of the shared counter which is 7. Both threads then increment the counter and write their incremented value directly back to main memory, where T2 does not see T1's incrementation of thecounterin main memory. T2 now effectively "ignores" T1's incrementation, and sets the value to 7, even though the correct value would have been 8.

So, as you can see, if two threads are incrementing the shared counter simultaneously, an error can still occur, even if both write the incremented value back to main memory as soon as it has been incremented.

To make the read-test-write operation a single atomic operation, you could use one of the atomic primitives in thejava.util.concurrentpackage. For instance, you could use AtomicInteger or AtomicLong.

I also have a discussion of atomic compare-and-swap operations in my Java compare and swap tutorial.

The Java volatile Happens-Before Guarantee

Since Java 5 thevolatilekeyword guarantees more than just the reading from and writing to main memory of variables. Actually, thevolatilekeyword guarantees this:

  • If Thread A writes to a volatile variable and Thread B subsequently reads the same volatile variable, then all variables visible to Thread A before writing the volatile variable, will also be visible to Thread B after it has read the volatile variable.

  • The reading and writing instructions of volatile variables cannot be reordered by the JVM (the JVM may reorder instructions for performance reasons as long as the JVM detects no change in program behaviour from the reordering). Instructions before and after can be reordered, but the volatile read or write cannot be mixed with these instructions. Whatever instructions follow a read or write of a volatile variable are guaranteed to happen after the read or write.

These statements require a deeper explanation.

When a thread writes to a volatile variable, then not just the volatile variable itself is written to main memory. Also all other variables changed by the thread before writing to the volatile variable are also flushed to main memory. When a thread reads a volatile variable it will also read all other variables from main memory which were flushed to main memory together 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;

Since Thread A writes the non-volatile variablesharedObject.nonVolatilebefore writing to the volatilesharedObject.counter, then bothsharedObject.nonVolatileandsharedObject.counterare written to main memory when Thread A writes tosharedObject.counter(thevolatilevariable).

Since Thread B starts by reading the volatilesharedObject.counter, then both thesharedObject.counterandsharedObject.nonVolatileare read from main memory into the CPU cache used by Thread B. By the time Thread B readssharedObject.nonVolatileit will see the value written by Thread A.

Developers may use this extended visibility guarantee to optimize the visibility of variables between threads. Instead of declaring each and every variablevolatile, only one or a few need be declaredvolatile. Here is an example of a simpleExchangerclass written after that principle:

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

Thread A may be putting objects from time to time by callingput(). Thread B may take objects from time to time by callingtake(). ThisExchangercan work just fine using avolatilevariable (without the use ofsynchronizedblocks), as long as only Thread A callsput()and only Thread B callstake().

However, the JVM may reorder Java instructions to optimize performance, if the JVM can do so without changing the semantics of the reordered instructions. What would happen if the JVM switched the order of the reads and writes insideput()andtake()? What ifput()was really executed like this:

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

Notice the write to thevolatilevariablehasNewObjectis now executed before the new object is actually set. To the JVM this may look completely valid. The values of the two write instructions do not depend on each other.

However, reordering the instruction execution would harm the visibility of theobjectvariable. First of all, Thread B might seehasNewObjectset totruebefore Thread A has actually written a new value to theobjectvariable. Second, there is now not even a guarantee about when the new value written toobjectwill be flushed back to main memory (well - the next time Thread A writes to a volatile variable somewhere...).

To prevent situations like the one described above from occurring, thevolatilekeyword comes with a "happens before guarantee". The happens before guarantee guarantees that read and write instructions ofvolatilevariables cannot be reordered. Instructions before and after can be reordered, but the volatile read/write instruction cannot be reordered with any instruction occurring before or after it.

Look at this example:

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;

The JVM may reorder the first 3 instructions, as long as all of themhappens beforethevolatilewrite instruction (they must all be executed before the volatile write instruction).

Similarly, the JVM may reorder the last 3 instructions as long as the volatile write instructionhappens beforeall of them. None of the last 3 instructions can be reordered to before the volatile write instruction.

That is basically the meaning of the Java volatile happens before guarantee.

理解

volatile is Not Always Enough

Even if thevolatilekeyword guarantees that all reads of avolatilevariable are read directly from main memory, and all writes to avolatilevariable are written directly to main memory, there are still situations where it is not enough to declare a variablevolatile.

In the situation explained earlier where only Thread 1 writes to the sharedcountervariable, declaring thecountervariablevolatileis enough to make sure that Thread 2 always sees the latest written value.

In fact, multiple threads could even be writing to a sharedvolatilevariable, and still have the correct value stored in main memory, if the new value written to the variable does not depend on its previous value. In other words, if a thread writing a value to the sharedvolatilevariable does not first need to read its value to figure out its next value.

As soon as a thread needs to first read the value of avolatilevariable, and based on that value generate a new value for the sharedvolatilevariable, avolatilevariable is no longer enough to guarantee correct visibility. The short time gap in between the reading of thevolatilevariable and the writing of its new value, creates anrace conditionwhere multiple threads might read the same value of thevolatilevariable, generate a new value for the variable, and when writing the value back to main memory - overwrite each other's values.

The situation where multiple threads are incrementing the same counter is exactly such a situation where avolatilevariable is not enough. The following sections explain this case in more detail.

Imagine if Thread 1 reads a sharedcountervariable with the value 0 into its CPU cache, increment it to 1 and not write the changed value back into main memory. Thread 2 could then read the samecountervariable from main memory where the value of the variable is still 0, into its own CPU cache. Thread 2 could then also increment the counter to 1, and also not write it back to main memory. This situation is illustrated in the diagram below:

Thread 1 and Thread 2 are now practically out of sync. The real value of the sharedcountervariable should have been 2, but each of the threads has the value 1 for the variable in their CPU caches, and in main memory the value is still 0. It is a mess! Even if the threads eventually write their value for the sharedcountervariable back to main memory, the value will be wrong.

When is volatile Enough?

As I have mentioned earlier, if two threads are both reading and writing to a shared variable, then using thevolatilekeyword for that is not enough. You need to use a synchronized in that case to guarantee that the reading and writing of the variable is atomic. Reading or writing a volatile variable does not block threads reading or writing. For this to happen you must use thesynchronizedkeyword around critical sections.

As an alternative to asynchronizedblock you could also use one of the many atomic data types found in thejava.util.concurrentpackage. For instance, theAtomicLongorAtomicReferenceor one of the others.

In case only one thread reads and writes the value of a volatile variable and other threads only read the variable, then the reading threads are guaranteed to see the latest value written to the volatile variable. Without making the variable volatile, this would not be guaranteed.

Thevolatilekeyword is guaranteed to work on 32 bit and 64 variables.

Performance Considerations of volatile

Reading and writing of volatile variables causes the variable to be read or written to main memory. Reading from and writing to main memory is more expensive than accessing the CPU cache. Accessing volatile variables also prevent instruction reordering which is a normal performance enhancement technique. Thus, you should only use volatile variables when you really need to enforce visibility of variables.


作者:KE meng

链接:https://www.zhihu.com/question/31459750/answer/52061391

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

来看这个代码:

int fun(int
&
 a)
{
    int b = a;
    int c = a;
    return a+b+c;
}

int main()
{
    int a=1;
    //.........做一些和a无关的事
    return fun(a);
}

这个代码是很好优化的,因为编译器知道a的值是1,参考上下文,编译器又能知道b和c的值也是1,

而且根本没有人用到了a,b,c三个变量,也没有任何人在修改a,b,c三个的值,所以编译器可能就直接

把这个函数优化成:

int main() { return 3; }

了.

这么优化有什么问题吗? 单线程没问题,但多线程就有问题了,如果是多线程,
a的值虽然在当前上下文中不会被修改,但可能正在被其他线程修改啊.于是上面的优化
就不对了. 那么,volatile关键字在这里就可以帮助我们了,volatile关键字提醒编译器:
a可能随时被意外修改.
意外的意思是虽然当前这段代码里看起来a不会变,但可能别的地方正在修改a的值哦.
所谓"别的地方",某些情况下指的就是其他线程了.

那么,如果把代码修改如下:

int fun(volatile int
&
 a)
{
    int b = a;
    int c = a;
    return a+b+c;
}

int main()
{
    volatile int a=1;
    //.........做一些和a无关的事
    return fun(a);
}

编译器就不敢优化了:

int fun(volatile int
&
 a)
{
    int b = a; //这里从内存读一下a吧,谁知道a还等不等于1呢
    int c = a; //这里再从内存读一下a吧,谁知道a还等不等于1呢
    return a+b+c;  //这里也从内存读一下a吧,谁知道a还等不等于1呢
}

int main()
{
    volatile int a=1;
    //.........做一些和a无关的事
    return fun(a); //完全不敢优化啊,鬼知道a变成多少了....
}

同理的,这段代码:

//..........
int a=0;
//做一些和a无关的事
if(a==0) doSomething();
//..........

编译器会发现,a肯定等于0啊,那我还if个毛啊,直接优化掉!

//..........
int a=0;
//做一些和a无关的事
doSomething(); //if被去掉了
//..........

但,一旦添加了volatile,编译器就不敢优化了

volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。

  先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

  下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

  那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

  但是用volatile修饰之后就变得不一样了:

  第一:使用volatile关键字会强制将修改的值立即写入主存;

  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

  第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

  那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

  那么线程1读取到的就是最新的正确的值。

volatile保证原子性吗?

  从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?

public class Test {
    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }

        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

  可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。

  这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。

把上面的代码改成以下任何一种都可以达到效果:

  • 采用synchronized:
  • Lock
  • AtomicInteger

results matching ""

    No results matching ""