public class TaskRunner {
private static int number; private static boolean ready; private static class Reader extends Thread {
public void run() { while (! ready) { Thread.yield(); } System.out.println(number); }Copy the code
} public static void main(String[] args) { new Reader().start(); number = 42; ready = true; The TaskRunner class maintains two simple variables. In its main method, it creates another thread that spins on the ready variable as long as it is false. When the variable becomes true, the thread prints the number variable.
We expect the program to simply print 42 after a short delay. In practice, however, the delay could be much longer. It might even hang forever, or even print a 0.
These exceptions are due to a lack of proper memory visibility, reordering, and, for the purposes of this article, the use of the volatile keyword to modify variables.
Memory visibility Simply put, multiple threads run on multiple cpus, and each thread has its own cache. Therefore, there is no guarantee of the order in which data is read from main memory, that is, there is no guarantee of the consistency of variable data read by threads on different cpus.
Combined with the above program, the main thread keeps copies of ready and number in its core cache, and the Reader thread does the same, after which the main thread updates the cached values. On most modern processors, write requests are not applied immediately after they are issued. In fact, the processor tends to queue these writes in a special write buffer. After a period of time, they write it to main memory all at once.
So when the main thread updates the number and ready variables, there is no guarantee of what the www.pizei.comReader thread will see. In other words, the Reader thread might see the updated value immediately, with some delay, or not at all.
As mentioned above, in addition to continuing in an infinite loop, the program has a low probability of printing a 0, which is why it resorts. When the CPU executes the instruction, it updates the ready variable and then executes the thread operation.
Reordering is an optimization technique used to improve performance that may be applied to different components:
The processor can flush its write buffers in any order other than program order. The processor may apply out-of-order execution techniques. The JIT compiler can optimize the volatile keyword by reordering.
The volatile keyword prefixes the variable with the Lock instruction at assembly time and ensures visibility between threads through the MESI cache consistency protocol. Changes made by any thread to a variable are synchronized to all threads that read the variable at the same time. Simply put, one change guarantees that all changes are made.
Here first look at the assembly layer Lock instruction, the early CPU to Lock the bus to achieve this instruction, the mediator chooses a CPU exclusive bus, so that other CPUS can not communicate with memory through the bus, atomic; Of course, this method is inefficient, and cache locking is generally adopted at present. Data consistency in this scenario is accomplished through MESI cache consistency protocol.
The cache consistency protocol is not detailed here, but the idea is that the CPU constantly sniffs the data exchange on the bus, and when a cache reads or writes to memory on behalf of its own CPU, other cpus are notified to synchronize their caches.
In the Java memory model, there are atomic operations that are critical to controlling concurrency in the Java memory model.
Load: writes data from main memory to working memory. Use: reads data from working memory to calculate assign: reassigns the calculated values to the working memory store: Write: assigns the value of a variable in the store past to a variable in the main memory lock: locks a variable in the main memory and identifies it as a thread-exclusive state UNLOCK: Unlock the main memory variable so that it can be locked by other threads. With volatile, the store and write operations must be consecutive and combined as atomic operations. The changes must be immediately synchronized to main memory, and must be flushed from main memory when used, thus ensuring visibility from volatile.
The volatile keyword also uses memory barriers to prevent instruction reordering. The memory visibility impact of volatile variables extends beyond the volatile variables themselves.
More specifically, suppose thread A writes A volatile variable, and thread B reads the same volatile variable. In this case, in writing if relative electron has more intuitive understanding, it can also refer to its format as follows:
Swim page http:www.pizei.com
Technically, any write to a volatile field occurs before each subsequent read of the same field. This is the volatile variable rule for the Java memory model.
Because of the advantage of memory sorting, we can sometimes piggyback on the visibility property of volatile as another variable. For example, in our example, we simply mark the ready variable as volatile:
public class TaskRunner {
private static int number; // not volatile private volatile static boolean ready; // same as before} After reading the ready variable, anything before writing the ready variable to true is visible to everything. Therefore, the number variable carries with it the memory visibility enforced by the ready variable. In short, even if it is not volatile, it behaves as volatile.
By leveraging these semantics, we can make the few variables in the class volatile and optimize visibility.