The problem

(1) How does volatile guarantee visibility?

(2) How does volatile prohibit reordering?

(3) How does volatile work?

(4) Pitfalls of volatile?

Introduction to the

Volatile is arguably the lightest synchronization mechanism provided by the Java VIRTUAL machine, but it is so poorly understood that many people are not used to it and use synchronized or another lock to solve multithreading problems.

Understanding the semantics of volatile is important to understanding the nature of multithreading, so Tong wrote an article explaining what the semantics of volatile are.

Semantics 1: Visibility

When we introduced the Java memory model earlier, we said that visibility means that when one thread changes the value of a shared variable, other threads immediately sense the change.

Please refer to JMM (Java Memory Model) for an explanation of the Java Memory Model.

And ordinary variable cannot perceive this immediately, and the value of the variable passed between threads are needs to be done through the main memory, for example, thread A modified A common variable values, and then write back to main memory, another thread B only after the completion of A thread A back to write to read the value of A variable from the main memory, will be able to read the value of the variable to the new, That is, the new variable is visible to thread B.

Inconsistencies may occur during this period, such as:

(1) Thread A does not write back immediately after modification;

(Line A changed the value of variable x to 5, but did not write back, thread B read the old value of 0 from main memory)

(2) Thread B is still using values in its own working memory, instead of reading values from main memory immediately;

(Thread A writes x 5 back to main memory, but thread B hasn’t read the main memory yet and is still using the old value 0.)

In both cases, ordinary variables cannot immediately perceive this.

However, volatile variables can be immediately aware of this, that is, volatile can guarantee visibility.

The Java memory model specifies that each change to a volatile variable must be written back to main memory immediately, and that each use of a volatile variable must flush its latest value from main memory.

The visibility of volatile can be seen in the following example:

public class VolatileTest {
    // public static int finished = 0;
    public static volatile int finished = 0;

    private static void checkFinished(a) {
        while (finished == 0) {
            // do nothing
        }
        System.out.println("finished");
    }

    private static void finish(a) {
        finished = 1;
    }

    public static void main(String[] args) throws InterruptedException {
        // Start a thread to check whether it is finished
        new Thread(() -> checkFinished()).start();

        Thread.sleep(100);

        // The main thread sets the FINISHED flag to 1
        finish();

        System.out.println("main finished"); }}Copy the code

In the above code, for the FINISHED variable, the program terminates normally with volatile modifices, and never with volatile modifices.

Because when volatile is not used, the thread checkFinished() reads the value of its own working memory variable each time, which is always zero, it never breaks out of the while loop.

With volatile, the thread of checkFinished() loads the latest value from main memory each time, and it immediately senses when finished is changed to 1 by the main thread and breaks out of the while loop.

Semantic two: Disallow reordering

Earlier, when we introduced the Java memory model, we said that orderliness in Java can be summed up as: if you look at it in this thread, all operations are ordered; If viewed from another thread, all operations are out of order.

The first half of the sentence refers to the sequential semantics in the thread, and the second half refers to the “instruction reordering” phenomenon and “working memory and main memory synchronization delay” phenomenon.

Please refer to JMM (Java Memory Model) for an explanation of the Java Memory Model.

Ordinary variable will only guarantee in the implementation process of this method in all depend on the place of assignment results can get the right result, and cannot guarantee variable assignment operation sequence and procedure in the code execution order is consistent, because one thread during the execution of a method cannot perceive this, which is characterized by serial semantic “within” thread.

For example, the following code:

// Two operations in one thread
int i = 0;
int j = 1;
Copy the code

Int j = 1; int j = 1; int j = 1; This sentence, reordering, is not perceptible in the thread.

It doesn’t seem to matter, but what about in a multi-threaded environment?

Let’s look at another example:

public class VolatileTest3 {
    private static Config config = null;
    private static volatile boolean initialized = false;

    public static void main(String[] args) {
        Thread 1 initializes the configuration information
        new Thread(() -> {
            config = new Config();
            config.name = "config";
            initialized = true;
        }).start();

        Thread 2 detects that configuration initialization is complete and uses configuration information
        new Thread(() -> {
            while(! initialized) { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
            }

            // do sth with configString name = config.name; }).start(); }}class Config {
    String name;
}
Copy the code

This example is very simple. Thread 1 initializes the configuration. Thread 2 detects that the configuration is initialized and uses it to do something.

In this example, if Initialized does not use volatile, reordering may occur. For example, if initialized is set to true before the initial configuration, thread 2 reads true and uses the configuration, which may result in an error.

(This example is only used to illustrate reordering, which is hard to come by in real time.)

Through this example, Tong Ge believes that “if you observe in this thread, all operations are orderly; Observing in another thread, all operations are out of order “has a deeper understanding.

Therefore, reordering is viewed from the perspective of another thread, because the effect of reordering is not felt in this thread.

Volatile variables, on the other hand, prohibit reordering, ensuring that the program is actually running in code order.

Implementation: Memory barrier

Volatile guarantees visibility and disallows reordering. How does it work?

The answer is a memory barrier.

Memory barriers serve two purposes:

(1) Prevent reordering of instructions on both sides of the barrier;

(2) Force the write buffer/cache data to write back to the main memory, so that the corresponding data in the cache is invalid;

As for the “memory barrier”, there is no complete agreement between the two sides. Therefore, the following article is helpful for those who are interested:

(Note that the official account does not allow external links, so you can only copy the link to the browser to read, and may need to scientific online)

(1) The JSR-133 Cookbook for Compiler Writers by Doug Lea

G.oswego.edu/dl/jmm/cook…

Doug Lea is the author of the Java package, Daniel!

(2) Memory Barriers/Fences by Martin Thompson

Mechanical-sympathy.blogspot.com/2011/07/mem…

Martin Thompson is focused on pushing performance to the limit and thinking about problems at the hardware level, such as how to avoid fake sharing, Daniel!

Its blog address is the address above, there are a lot of low-level knowledge, interested can go to see.

(3) Dennis Byrne’s Memory Barriers and JVM Concurrency

www.infoq.com/articles/me…

This article, which I think is well written, basically combines the two viewpoints and analyzes the implementation of memory barriers at the assembly level.

At present, there are no more than three articles on memory barriers in the domestic market, including the introduction of related books.

Let’s look at an example to understand the effect of memory barriers:

public class VolatileTest4 {
    // a does not use volatile modifier
    public static long a = 0;
    // Eliminate the effect of cached rows
    public static long p1, p2, p3, p4, p5, p6, p7;
    // b uses the volatile modifier
    public static volatile long b = 0;
    // Eliminate the effect of cached rows
    public static long q1, q2, q3, q4, q5, q6, q7;
    // c does not use volatile modifier
    public static long c = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (a == 0) {
                long x = b;
            }
            System.out.println("a=" + a);
        }).start();

        new Thread(()->{
            while (c == 0) {
                long x = b;
            }
            System.out.println("c=" + c);
        }).start();

        Thread.sleep(100);

        a = 1;
        b = 1;
        c = 1; }}Copy the code

In this code, a and C do not use volatile, and B uses volatile, and we add seven long fields between A/B and B/C to eliminate the effect of pseudo-sharing.

For more information about false sharing, please check out tong Ge’s previous article “What is false sharing?” .

In a while loop for two threads of A and C we get B, and guess what? If I take long x = b; What about this row? Let’s run it.

The effect of a volatile variable is not only on itself, but also on reading and writing values above and below it.

defects

Now that we’ve seen the semantics of the volatile keyword, is it universal?

Of course not. Have you forgotten the three characteristics of consistency that we discussed in the memory model chapter?

Consistency mainly contains three characteristics: atomicity, visibility and order.

The volatile keyword guarantees visibility and orderliness. Does volatile guarantee atomicity?

Look at the following example:

public class VolatileTest5 {
    public static volatile int counter = 0;

    public static void increment(a) {
        counter++;
    }

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(100);
        IntStream.range(0.100).forEach(i->
                new Thread(()-> {
                    IntStream.range(0.1000).forEach(j->increment()); countDownLatch.countDown(); }).start()); countDownLatch.await(); System.out.println(counter); }}Copy the code

In this code, we have 100 threads incrementing counter 1000 times each, which should be a total increment of 100000, but the actual result never reaches 100000.

Let’s look at the bytecode of the increment() method:

0 getstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
3 iconst_1
4 iadd
5 putstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
8 return
Copy the code

You can see that counter++ is broken down into four instructions:

(1) getStatic, get the current value of counter into the stack

(2) iconst_1, push int value 1

(3) iAdd, add the two values at the top of the stack

(4) putStatic, write the result of the addition back to counter

Since counter is volatile, getStatic flushs the latest value from main memory, and putStatic immediately synchronizes the changed value to main memory.

However, the middle steps iconst_1 and iadd may have changed the value of counter during execution and have not re-read the latest value in main memory, so volatile does not guarantee atomicity in the case of Counter ++.

The volatile keyword guarantees visibility and order, not atomicity, which can only be solved by locking or using atomic classes.

Further, we derive scenarios where the volatile keyword is used:

(1) The result of the operation does not depend on the current value of the variable, or can ensure that only a single thread changes the value of the variable;

(2) Variables do not need to participate in invariant constraints together with other state variables.

In other words, volatile does not guarantee atomicity, so other constraints must be added to make the scene atomic.

Such as:

private volatile int a = 0;

/ / thread A
a = 1;

/ / thread B
if (a == 1) {
    // do sth
}
Copy the code

a = 1; The assignment is atomic, so it can be volatile.

conclusion

(1) The volatile keyword guarantees visibility;

(2) The volatile keyword guarantees order;

(3) The volatile keyword does not guarantee atomicity;

(4) The underlying volatile keyword is implemented primarily through memory barriers;

(5) The use of volatile must be atomic;

eggs

There are three articles about “memory barrier”. Considering that some students can’t surf the Internet scientifically, Tong Ge downloaded these three articles and sorted them out.

Pay attention to my public number “Tong Elder brother read source code”, the background reply “volatile”, download these three materials.


Welcome to pay attention to my public number “Tong Elder brother read source code”, view more source code series articles, with Tong elder brother tour the ocean of source code.