Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.


In the last article, we looked at the use and some of the principles of the synchronized keyword, and now we look at another very important keyword in concurrent programming, volatile.

To get an idea of what volatile can do, let’s look at some code:

public class VolatileTest {
    private static boolean flag=false;

    public void setFlag(a){
        this.flag=true;
        System.out.println(Thread.currentThread().getName()+" change flag to true");
    }

    public void getFlag(a){
        while(! flag){ } System.out.println(Thread.currentThread().getName()+" get flag status change to true");
    }

    public static void main(String[] args) {
        VolatileTest test=new VolatileTest();
        new Thread(test::getFlag).start();
        
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        newThread(test::setFlag).start(); }}Copy the code

The example uses two threads to modify and read Boolean flags. When the thread executing the getFlag method detects that flag is true, it should exit the loop and print the statement. But if you look at the result, you’ll see that only statements in the setFlag method are printed and the program never finishes executing.

Now, we add the volatile keyword to flag and execute the above code again:

 private static volatile boolean flag=false;
Copy the code

As you can see, the thread of the getFlag method detects the flag change and terminates the program as normal. Combining the above examples, we found that volatile solved the problem of data inconsistencies when one thread wrote data and another thread read data. So it’s time to introduce the Java Memory Model (JMM) to see what volatile does.

As shown in the figure above, threads running in Java cannot read or write variables directly from main memory, but can only manipulate variables in their own working memory and then synchronize them to main memory. Main memory is shared by multiple threads. Single threads do not share working memory. If threads need to communicate with each other, they must use main memory relay to complete the communication.

In JMM control, operations on data atoms are divided into the following eight categories:

  • read(Read) : Reads data from main memory
  • load(Load) : Writes data read from main memory to working memory
  • use(use) : Reads values from working memory to calculate
  • assign(Assignment) : Reassigns the calculated value to the working memory
  • storeStorage: Writes data from the working memory to the main memory
  • write(Write) : willstorePast variable values are assigned to variables in main memory
  • lock(lock) : Locks the main memory variable, marking it as thread-exclusive
  • unlock(Unlock) : Unlocks the main memory variable. After unlocking, other threads can lock the variable

Then, our previous example can be represented by the following graph:

Obviously, thread 1 can’t break out of the loop because it keeps reading the flag in its own working memory instead of getting the updated value in main memory.

To address cache consistency issues, a bus locking solution has been used. In particular, when a CPU reads data from main memory into the cache, it locks the data on the bus so that no other CPU can read or write the data until the CPU runs out of data and releases the lock.

However, because the granularity of locking is too large, the blocking time is too long, which seriously reduces the CPU performance. So on top of that, rows became the MESI cache consistency protocol we now use:

In simple terms, multiple cpus read the same data from main memory into their caches. When one CPU modifies the data in the cache, the data is immediately synchronized back to main memory. The other cpus can sense the change through the bus sniffing mechanism and invalidate the data in their caches.

To summarize, a read operation reads data from memory into the cache without doing anything. During write operations, a signal is sent to other cpus to invalidate the variable. Other cpus can only access the variable from memory.

Configure startup parameters for the test class and print assembly instructions to the console:

-server -Xcomp -XX:+UnlockDiagnosticVMOptions 
-XX:+PrintAssembly 
-XX:CompileCommand=compileonly,*VolatileTest.setFlag
Copy the code

As can be seen, when executing the statement to modify flag, the prefix lock instruction is added first to achieve the lock of the cache line. To put it simply:

lock
flag=trueWrite Back to primary memory unlockCopy the code

The lock is added only when the write operation is performed. Compared with the bus, data is locked, which greatly reduces the granularity of the lock. As long as the data is not in the write process, other threads can still read the data in the main memory, thus improving the CPU performance

In addition, volatile enables ordering of instructions. Order is guaranteed because sometimes the code is actually executed in a different order than the code we typed. Why this happens, then, is that it is necessary to introduce instruction reordering: the compiler reorders bytecode instructions to optimize program performance.

The basis for instruction reordering is that the compiler assumes that the result of the run must be normal. In a single thread, instruction reordering must be positive to the program, which can optimize the performance of the program, but in a multi-thread, there may be some problems because of instruction reordering. Volatile implementation orderliness guarantees two things:

  • volatilePrevious code cannot be adjusted after it
  • volatileSubsequent code cannot be moved to the front of it

conclusion

Finally, based on the previous article, we summarize the characteristics and differences between synchronized and volatile:

  • Differences in use:volatileYou can only modify variables,synchronizedCan modify methods and statement blocks
  • Assurance of atomicity:synchronizedCan guarantee atomicity,volatileAtomicity is not guaranteed
  • Guarantee of visibility: Both can guarantee visibility, but the implementation principle is different.volatileI add to the variablelock.synchronizedusemonitorenterandmonitorexit
  • 2. A guarantee of order:volatileTo ensure order,synchronizedOrderliness can also be guaranteed, but at a higher cost (heavyweight), concurrency degrades to serial execution
  • In addition:synchronizedCan cause a blockage, andvolatileDoes not cause blocking