Java Threads - The volatile keyword

在网上看到很多关于 volatile 关键字的说明和使用问题, 今天引用Java Threads中的解释,感觉全面而详细,可惜是英文的.

这里很清晰的揭示了volatile 本身并不处理java读取数据的原子性问题,而是强制线程对数据的读写必须及时反映到主内存.

以下说明了volatile时, 其不能用于 long 和double 类型数据的根本和直接原因,其他很多解释都犯了概念性的错误.

其他原子类型数据的loading 和storing的原子性是由Java语言自身定义的,并不是由volatile来修饰实现的,这里很多网上的相关解释存在概念上的错误。

 

The Volatile Keyword

There is still one more threading issue in this example, and it has to do with the setDone() method. This method is called from the event-dispatching thread when the Stop button is pressed; it is called by an event handler (an actionPerformed() method) that is defined as an inner class to the SwingTypeTester class. The issue here is that this method is executed by the event-dispatching thread and changes data that is being used by another thread: the done flag, which is accessed by the thread of the AnimatedDisplayCanvas class.

So, can't we just synchronize the two methods, just as we did previously? Yes and no. Yes, Java's synchronized keyword allows this problem to be fixed. But no, the techniques that we have learned so far will not work. The reason has to do with the run() method. If we synchronized both the run() and setDone() methods, how would the setDone() method ever execute? The run( ) method does not exit until the done flag is set, but the done flag can't be set because the setDone() method can't execute until the run() method completes.

 

Definition: Scope of a Lock

The scope of a lock is defined as the period of time between when the lock is grabbed and released. In our examples so far, we have used only synchronized methods; this means that the scope of these locks is the period of time it takes to execute the methods. This is referred to as method scope.

Later in this chapter, we'll examine locks that apply to any block of code inside a method or that can be explicitly grabbed and released; these locks have a different scope. We'll examine this concept of scope as locks of various types are introduced.

 

The problem at this point relates to the scope of the lock: the scope of the run() method is too large. By synchronizing the run() method, the lock is grabbed and never released. There is a way to shrink the scope of a lock by synchronizing only the portion of the run() method that protects the done flag (which we examine later in this chapter). However, there is a more elegant solution in this case.

The setDone() method performs only one operation with the done flag: it stores a value into the flag. The run() method also performs one operation with the done flag: it reads the value during each iteration of the loop. Furthermore, it does not matter if the value changes during the iteration of these methods, as each loop must complete anyway.

The issue here is that we potentially have a race condition because one piece of data is being shared between two different threads. In our first example, the race condition came about because the threads were accessing multiple pieces of data and there was no way to update all of them atomically without using the synchronized keyword. When only a single piece of data is involved, there is a different solution.

Java specifies that basic loading and storing of variables (except for long and double variables) is atomic. That means the value of the variable can't be found in an interim state during the store, nor can it be changed in the middle of loading the variable to a register. The setDone() method has only one store operation; therefore, it is atomic. The run( ) method has only one read operation. Since the rest of the run() method does not depend on the value of the variable remaining constant, the race condition should not exist in this case.

Unfortunately, Java's memory model is a bit more complex. Threads are allowed to hold the values of variables in local memory (e.g., in a machine register). In that case, when one thread changes the value of the variable, another thread may not see the changed variable. This is particularly true in loops that are controlled by a variable (like the done flag that we are using to terminate the thread): the looping thread may have already loaded the value of the variable into a register and does not necessarily notice when another thread changes the variable.

One way to solve this problem is to provide setter and getter methods for the variable. We can then simply synchronize access by using the synchronized keyword on these methods. This works because acquiring a synchronization lock means that all temporary values stored in registers are flushed to main memory. However, Java provides a more elegant solution: the volatile keyword. If a variable is marked as volatile, every time the variable is used it must be read from main memory. Similarly, every time the variable is written, the value must be stored in main memory. Since these operations are atomic, we can avoid the race condition in our example by marking our done flag as volatile.

In most releases of the virtual machine prior to JDK 1.2, the actual implementation of Java's memory model made using volatile variables a moot point: variables were always read from main memory. In subsequent iterations of Java, up to and including J2SE 5.0, implementations of virtual machines became more sophisticated and introduced new memory models and optimizations: this trend is expected to continue in future versions of Java. With all modern virtual machine implementations, developers can not assume that variables will be accessed directly from main memory.

So why is volatile necessary? Or even useful? Volatile variables solve only the problem introduced by Java's memory model. They can be used only when the operations that use the variable are atomic, meaning the methods that access the variable must use only a single load or store. If the method has other code, that code may not depend on the variable changing its value during its operation. For example, operations like increment and decrement (e.g., ++ and --) can't be used on a volatile variable because these operations are syntactic sugar for a load, change, and a store.

As we mentioned, we could have solved this problem by using synchronized setter and getter methods to access the variable. However, that would be fairly complex. We must invoke another method, including setting up parameters and the return variable. We must grab and release the lock necessary to invoke the method. And all for a single line of code, with one atomic operation, that is called many times within a loop. The concept of using a done flag is common enough that we can make a very strong case for the volatile keyword.

The requirements of using volatile variables seem overly restrictive. Are they really important? This question can lead to an unending debate. For now, it is better to think of the volatile keyword as a way to force the virtual machine not to make temporary copies of a variable. While we can agree that you might not use these types of variables in many cases, they are an option during program design. In Chapter 5, we examine similar variables (atomic variables) that are less restrictive: variables that are not only atomic but can be built on using programming techniques. This allows us to build complex atomic functionality.

How does volatile work with arrays? Declaring an array volatile makes the array reference itself volatile. The elements within the array are not volatile; the virtual machine may still store copies of individual elements in local registers. There is no way to specify that the elements of an array should be treated as volatile. Consequently, if multiple threads are going to access array elements, they must use synchronization in order to protect the data. Atomic variables can also help in this situation.

 

posted @ 2013-12-29 19:56  Java Bugs  阅读(262)  评论(0编辑  收藏  举报