Detailed explanation of volatile keyword (from cache coherence)

Before explaining the volatile keyword, let's take a look at the concept of cache coherence in operating systems.

As we all know, the running speed of the CPU is much higher than the reading and writing speed of the main memory. During the running process, in order to exchange data, the CPU must frequently read and write data. The slow reading and writing speed of the main memory causes the throughput of the CPU to run. amount decreased. In order to solve this problem, the current machine will add a layer of cache (in fact, there are more than one layer, there are multiple layers). In the future, each time the CPU performs read and write operations, it will directly interact with the cache, and then store the cache in the cache. The data is flushed back to main memory. In a single-threaded situation this is fine, but in a multi-threaded environment it can cause some pitfalls. Because this will cause each thread to change the data in its own cache (even if the changed data is flushed from the cache back to the main memory), other variables read the data in the original cache, which This results in data invisibility. To prevent data invisibility, there is a cache coherence protocol protocol on the hardware side. One of the most famous MESI protocol.

Cache Coherence Protocol (MESI):

    When the CPU writes data, if it finds that the operating variable is a shared variable, that is, a copy of the variable exists in other CPUs, it will send a signal to notify other CPUs to invalidate the cache line of the variable, so when other CPUs need to read When fetching this variable, it finds that the cache line that caches the variable in its own cache is invalid, so it will be re-read from memory.

In the JVM, all variables are stored in the main memory, and each thread has its own working memory (that is, the high-speed cache mentioned earlier). That is, when accessing a shared variable, multiple java threads will have a copy of the shared variable in their own working memory. When a thread changes the updated data in its own working memory, the thread is blocked or for other reasons, and the updated data is not flushed back to the main memory in time, then other threads will delete the data from the main memory or its own working cache. The data read is still the old value, that is, the new value of the data is invisible to other threads. In order to prevent the invisibility of shared variable variables, java provides the volatile keyword to guarantee.

When a variable is modified by volatile, it actually has the following two meanings:
     1. When a thread modifies the variable, it will immediately flush the modified new value back to the main memory to ensure that the main memory is always up-to-date. The data of
     2. Cache line coherence protocol is imposed on this ratio variable. That is to say, after the current thread modifies the variable, the system will notify other threads that the data in their work cache is invalid, so when other threads want to read the variable again,
        they will from the main memory again, and then Make a copy in its working cache.

First let's look at a multithreaded example of a variable that is not modified by volatile:

package thread.volatile_learn;

public class no_volatile {
	private boolean flag = false; //Mark whether a resource is free
	public void doSomethind(){
		while(!flag){
			System.out.println("the resource is free ,let us do something");
		}
		if(flag){
			System.out.println("the resource is busy ,let us stop!");
		}
	}
	public static void main(String[] args) throws Exception {
		final no_volatile sharedObject = new no_volatile();
		
		new Thread(){
			public void run() {
				sharedObject.doSomethind();
			};
		}.start();
		
		Thread.sleep(3000);
		new Thread(){
			public void run() {
				sharedObject.flag=true;
			};
		}.start();
	}
}

This is a classic piece of code used to explain concurrent programming. The first thread (thread A) starts executing. Since falg=false, the while loop in the thread will continue. After A executes for a period of time, the second The thread (thread B) starts to execute, and the flag is set to true. At this time, thread A will continue to execute the loop body, or will it display "thre resource is busy, let us stop". In most cases, thread A will end immediately, because as we said earlier, the current jvm has actually implemented the cache coherence protocol, which means that when thread B modifies the flag shared variable, the system will try to store the thread in the working memory as much as possible. The variables are flushed back to main memory. But this is just as much as possible. If the thread turns to do other things at this time, and has not had time to brush the variables in the working memory back to the main memory, then although the flag cache of the working memory in thread A has expired at this time, restart from the main memory. What is stored and read is actually the original value. This causes thread A to always execute the loop body. (Actually, this code has been tested many times by me. I thought about blocking thread B after setting the flag, but the final result is that thread A will exit and will not always execute the loop body. This shows that in these examples I tested In fact, every time thread B flushes the modified value from the working memory back to the main memory in time. From this point, it can be seen that the current JVM has actually provided a good solution to the data visibility problem itself. support).

If we use volatile to modify the flag, then after each modification, the thread will immediately flush the modified variable in the working memory back to the main memory, so that other threads will always read the latest value. This ensures data visibility.

But the visibility of the data is guaranteed. Can volatile guarantee that the operation on the modified data is atomic? Let's first look at the following piece of code:


public class volatile_learn {
	private volatile int inc = 0; //volatile guarantees that when a shared variable is modified, the changed shared variable is immediately flushed from the working cache back to main memory.
	public void increase(){
		this.inc++;
	}
	public static void main(String[] args) {
		final volatile_learn sharedObject = new volatile_learn();
		for(int i=0;i<10;i++){
			new Thread(){
				public void run() {
					for(int j=0;j<100;j++){
						sharedObject.increase();
					}
				};
			}.start();
			
		}
		while(Thread.activeCount()>1){
			Thread.yield();
		}
		System.out.println(sharedObject.inc);
	}
}
According to what we said before, after each modification of the volatile modified variable, other threads can 'see' the modification, then the value read by all threads is the latest value, then executing this code, the result should be 1000. However, the answer is not. In the 10 times I tested, only two times were 1000, and the others were between 800 and 1000. In fact, this is because of the non-atomic nature of the auto-increment operation. In fact, the auto-increment operation seems to be a simple step, but in fact a total of three steps are performed:

1. Read the initial value of the variable (if it is the first time, copy the variable and put it into the working memory as a high-speed cache)

2. The cpu adds 1 to the operation

3. The cpu writes the modified value to the working memory.

If thread A now reads the value of inc (assuming the value is 10 at this time), thread A is blocked at this time, thread B starts to preempt cpu resources, continues to read the value of Inc in main memory, and puts a copy into its own In the working memory, then add 1 operation, and immediately after writing to the working memory (if volatile is not used, it is difficult to guarantee when the system will flush the main memory) back to the main memory (the volatile value is 11 at this time). At this time, thread A re-enters the runnable state and obtains cpu resources to start running. Since thread B has modified the value of Inc, the cache in thread A's working memory has been invalidated at this time, but thread A has already executed before blocking. The read operation of inc, so line A continues to perform the inc+1 operation. At this time, after the auto-increment operation is performed, the value written to the working memory Inc is 11, and finally it is flushed back to the main memory. Therefore, the final value in the main memory at this time is 11, not 12. Because of this, although the volatile modification is used, the final running result is still not what we expected. The reason is that volatile only guarantees the visibility of data, and does not guarantee the atomicity of operations on modified variables.

How to modify the above code so that we can get 1000? It's very simple. Just add the synchronized modification in front of the auto-increment method. At this time, the execution of the method is an atomic operation.




Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325941976&siteId=291194637