Original article & Experience summary & from the university to A factory all the way sunshine vicissitudes

For details, please click www.codercc.com

1. Introduction of volatile

In the last article we looked at the Java keyword synchronized, and we know that one of the most important tools in Java is volatile.

We learned from the previous article that synchronized is a blocking type of synchronization, which can be upgraded to a heavyweight lock when threads are highly competitive. Volatile is arguably the lightest synchronization mechanism provided by the Java Virtual Machine. But it’s also not easy to understand properly, and many programmers who encounter thread-safety issues in concurrent programming use synchronized. The Java memory model tells us that threads copy shared variables from main memory to working memory, and then the execution engine operates on the data in working memory. When does a thread write to main memory after performing an operation in working memory? The timing is not specified for common variables. However, the Java VIRTUAL Machine has a special convention for volatile variables. A change made to a volatile variable is immediately detected by other threads.

At this point, we have a general impression that volatile variables guarantee that each thread can get the latest value of the variable, thus avoiding dirty reads.

2. Implementation principles of volatile

How is volatile implemented? For example, a very simple Java code:

Instance = new Instancce() // Instance is a volatile variable

When generating assembly code, write to volatile shared variables will be prefixed with the Lock directive (you can use some tools to see this, but I’ll just give you the results here). We thought there must be something magical about the Lock directive, so what does the Lock prefix directive find on multicore processors? There are mainly two aspects of the influence:

  1. Write the current processor cache row data back to system memory;
  2. This write-back operation invalidates the memory address cached in other cpus

To increase processing speed, instead of directly communicating with memory, the processor reads system memory data into an internal cache (L1, L2, or other) before operating, but does not know when the data will be written to memory. If a volatile variable is written, the JVM sends an instruction prefixed with “Lock” to the processor to write the variable’s cached line back to system memory. However, even if it is written back into memory, it is problematic to perform the computation if the value in the cache of other processors is still old. So, on a multiprocessor, in order to ensure that each processor cache is consistent, can achieve cache coherence protocol, each processor by sniffing the spread of the data on the bus to check the value of the cache is expired, when the processor found himself cache line corresponding to the memory address has been changed, and will be set for the current processor cache line in invalid state, When the processor modifies the data, it reads the data back from system memory to the processor cache. Therefore, after analysis, we can draw the following conclusions:

  1. Instructions prefixed with Lock cause the processor cache to be written back into memory;
  2. One processor’s cache writing back to memory invalidates the other processor’s cache.
  3. When the processor finds that the local cache is invalid, it rereads the variable data from memory, that is, it can get the latest value.

This mechanism for volatile variables allows each thread to obtain the latest value of the variable.

3. The happens-before relationship of volatile

From the above analysis, we have learned that volatile variables can be used to ensure that each thread gets the latest value through the cache consistency protocol, which satisfies the “visibility” of the data. We continue the way of analyzing problems in the last article (I always think that the way of thinking about problems belongs to myself and is also the most important, and I am constantly cultivating my ability in this aspect). I have always divided the entry point of concurrent analysis into two cores and three properties. Two cores: the JMM memory model (main memory and working memory) and happens-before; Three properties: atomicity, visibility and order (the summary of the three properties will be discussed with you in the future). Without further further, let’s look at one of the two core elements: the happens-before relationship of volatile.

One of the six happens-before rules is: ** The volatile variable rule: a write to a volatile field happens before any subsequent read to that volatile field. ** We use this rule to derive:

public class VolatileExample { private int a = 0; private volatile boolean flag = false; public void writer(){ a = 1; //1 flag = true; //2 } public void reader(){ if(flag){ //3 int i = a; / / 4}}}Copy the code

The happens-before relationship corresponding to the above instance code is shown in the figure below:

Thread A executes the writer method, and thread B executes each arrow in the reader method diagram. The two nodes in the code have A happens-before relationship. The red is the happens-before of any subsequent reads of volatile based on the write of volatile, and the blue is the derivation based on the transitivity rule. Here 2 happens-before 3, also defined according to the happens-before rule: If A happens before B, then the result of A is visible to B, and the order in which A is executed precedes the order in which B is executed. We know that the result of operation 2 is visible to operation 3, which means that thread B is aware of the change in the volatile variable flag to true.

4. Memory semantics of volatile

After analyzing the happens-before relationship, we will now further analyze the memory semantics of volatile. (If you agree with my method, I would like to give you a good appreciation.) Little brother here thanks, is a encouragement to me). For example, if thread A executes writer and thread B executes Reader, then flag and A are in the initial state of the local memory.

When a volatile variable is written, the shared variable in the thread’s local memory becomes invalid, so thread B needs to read the latest value of the variable from main memory. The following diagram shows the memory change when thread B reads the same volatile variable.

In A horizontal sense, there’s A communication going on between thread A and thread B, and when thread A writes A volatile, it’s essentially sending A message to thread B telling thread B that all the values you have now are old, and then thread B reads that volatile as if it’s receiving the message that thread A just sent. Now that it’s old, what’s going to happen to thread B? Naturally, you have to go to main memory.

Ok, so now we have both cores: happens-before and memory semantics. If you are not satisfied, you suddenly find that you love learning so much (smile), then we will have some dry products —-volatile memory semantic implementation.

4.1 Memory semantic implementation of volatile

We all know that the JMM allows the compiler and processor to reorder instruction sequences for performance optimization without changing the correct semantics, but what if you want to prevent reordering? The answer is that you can add memory barriers.

The memory barrier

There are four types of JMM memory barriers.

The Java compiler inserts memory barrier instructions in place when generating the instruction series to prevent reordering of certain types of processors. In order to implement the memory semantics of volatile, the JMM will restrict the reordering of certain types of compilers and processors. The JMM will specify a volatile reordering table for compilers:

“NO” means that reordering is forbidden. To implement volatile memory semantics, when the compiler generates bytecode, it inserts a memory barrier in the instruction sequence to prevent reordering of certain types of processors. It is almost impossible for the compiler to find an optimal placement that minimizes the total number of insertion barriers, so the JMM takes a conservative approach:

  1. Insert a StoreStore barrier before each volatile write;
  2. Insert a StoreLoad barrier after each volatile write;
  3. Insert a LoadLoad barrier after each volatile read;
  4. Insert a LoadStore barrier after each volatile read operation.

Note that volatile writes insert memory barriers before and after, while volatile reads insert two memory barriers after

StoreStore barrier: prohibits reordering of normal writes above and volatile writes below;

StoreLoad barrier: Prevents volatile reads above from being reordered from volatile reads/writes below

LoadLoad barrier: disables all normal read operations below and volatile read reordering above

LoadStore barrier: disallows all normal write operations below and volatile read reorder above

Here are two sketches, taken from a fairly good book, The Art of Concurrent Programming in Java.

5. An example

Now that we understand the essence of volatile, I think we can answer the question at the beginning of this article. The corrected code is:

public class VolatileDemo { private static volatile boolean isOver = false; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { while (! isOver) ; }}); thread.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } isOver = true; }}Copy the code

Note that isOver is now set to volatile. Changing isOver to true in the main thread invalidates the value of this variable in the thread’s working memory and requires reading the value from main memory again. It is now possible to read the latest value of isOver to true to terminate the infinite loop in the thread, thus successfully stopping the thread. Now the problem is solved and the knowledge is learned :). (If you think it’s good, please like it. It’s an encouragement to me.)

reference

The Art of Concurrent Programming in Java