volatile,synchronized visibility, orderliness, atomic code proof (basic hard core)

0. Introduction

The previous article "Synchronized Usage Principles and Lock Optimization Upgrade Process" analyzes the synchronized keyword principle in detail from the perspective of interviews. This article mainly focuses on the volatile keyword and uses code to analyze visibility, atomicity, ordering, and synchronized also assists Prove it to deepen your understanding of locks.

**

1. Visibility

1.1 Invisibility

After thread A operates the shared variable, the shared variable is invisible to thread B. Let's look at the code below.

package com.duyang.thread.basic.volatiletest;
/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 10:10
 * @description: Invisibility test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                //Note that there can be no output here
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //Sleep for 100 milliseconds and let thread A execute first
        Thread.sleep(100);
        //The main thread sets the shared variable flag equal to false
        flag = false;
    }
}

In the above code, thread A is started in the main thread, the main thread sleeps for 100 milliseconds, the purpose is to let thread A execute first, the main thread finally sets the shared variable flag equal to false, the console does not output the result, and the program endless loop does not end. As shown in the figure below, after the main thread executes the flag=false, the Java Memory Model (JMM), the main thread sets the flag value of its own working memory to false and then synchronizes to the main memory. At this time, the main memory flag=false, and thread A does not The latest flag value (false) of the main memory is read, the main thread is executed, the working memory of thread A has been occupying the cpu time slice, and the latest flag value will not be updated from the main memory, thread A cannot see the latest value of the main memory, A The value used by the thread is inconsistent with the value used by the main thread, resulting in program confusion. This is the invisibility between threads, so you should be able to understand. Invisibility between threads is the root cause of the program's infinite loop.

1.2 volatile visibility

In the above case, we used the code to prove that the shared variables between threads are invisible. In fact, you can draw the conclusion from the above figure: as long as the working memory of thread A can sense the value of the shared variable flag in the main memory changes, it will be fine , so that the latest value can be updated to the working memory of thread A. As long as you can think of this, the problem is over. Yes, the volatile keyword implements this function, and thread A can perceive the main memory shared variable. The flag has changed, so it is forced to read the latest value of the flag from the main memory and set it to its own working memory, so if you want the VolatileTest code program to end normally, use the volatile keyword to modify the shared variable flag, private volatile static boolean flag = true; you're done . The hardware foundation of the underlying implementation of volatile is based on the hardware architecture and cache coherence protocol. If you want to dig deeper, you can read the previous article "What is Visibility?" (easy to understand)". You have to try it to get it!

1.3 synchronized visibility

synchronized ensures that shared variables are visible. Every time a lock is acquired, the latest shared variable is re-read from main memory.

package com.duyang.thread.basic.volatiletest;
/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 10:10
 * @description: Invisibility test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                synchronized (VolatileTest.class){
                    
                }
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //Sleep for 100 milliseconds and let thread A execute first
        Thread.sleep(100);
        //The main thread sets the shared variable flag equal to false
        flag = false;
    }
}

In the above code, I added a synchronized code block to the while loop of thread A, and synchronized (VolatileTest.class) locks the class of the VolatileTest class. The final program outputs "threadA over" and the program ends. It can be concluded that thread A will read the latest data of the main memory shared variable flag=false before each lock. This proves that the synchronized keyword and volatile have the same visibility semantics.

2. Atomicity

2.1 Atomicity

Atomicity means that an operation either succeeds or fails and is an indivisible whole.

2.2 volatile non-atomicity

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 11:22
 * @description: Volatile keyword atomicity test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileAtomicTest {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //The main thread waits for AB to finish executing!
        threadA.join();
        threadB.join();
        System.out.println("accumulate count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            for(int i=0; i<10000; i++) {
                count++;
            }
        }
    }

}

In the above code, threads A and B are started in the main thread, each thread increments the shared variable count value by 10,000 times, and the accumulated value of count is output after thread AB runs; the following figure is the console output result, the answer is not equal to 20000, It is proved that volatile-modified shared variables do not guarantee atomicity. The root cause of this problem is count++. This operation is not an atomic operation. In the JVM, count++ is divided into 3 steps.

  • Read count value.
  • Increment count by 1.
  • Write the count value to main memory.

Thread safety issues arise when multi-threaded operations on count++.

2.3 synchronized atomicity

We use the synchronized keyword to transform the above code.

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 11:22
 * @description: Volatile keyword atomicity test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileAtomicTest {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //The main thread waits for AB to finish executing!
        threadA.join();
        threadB.join();
        System.out.println("accumulate count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            //this locks the Task object instance, which is the task
            synchronized (this) {
                for(int i=0; i<10000; i++) {
                    count++;
                }
            }
        }
    }
}

In the above code, the synchronized (this) synchronization code block is added to the thread self-increment method. This locks the Task object instance, that is, the task object; the execution order of threads A and B is synchronized, so the final AB thread runs The result is 20000, and the console output is as shown below.

3. Orderliness

3.1 Orderliness

What is orderliness? The Java program code we write is not always executed in order, and there may be program reordering (instruction reordering). Go back to improve the overall operating efficiency. After drawing a simple diagram, give a practical application case code, and everyone will learn.

As shown in the figure above, task 1 takes a long time, while task 2 takes a short time. After JIT compiles the program, task 2 is executed first, and then task 1 is executed, which has no effect on the final running result of the program, but improves the efficiency (task 2 runs first No effect on the result, but improved responsiveness)!

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 15:09
 * @description: Instruction rearrangement test
 * @modified By: 
 * Public number: call practice
 */
public class CodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //Initialize 4 variables
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("number of executions:"+count);
                break;
            } else {
                System.out.println("number of executions:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

In the above code, the loop starts threads A and B. If x and y are both equal to 0, the program exits. count is the program count counter. The following figure is the result of printing part of the console program. It can be analyzed from the figure that when x and y are equal to 0, thread A's a = 3; x = b; the two lines of code are reordered, and in thread B, b = 3;y = a; two lines of code are also done Reorder. This is the result after the JIT compiler optimizes the code reordering.

3.2 volatile ordering

A shared variable modified by volatile is equivalent to a barrier. The function of the barrier is to not allow instructions to be rearranged at will. The orderliness is mainly manifested in the following three aspects.

3.2.1 The instructions above the barrier can be reordered.

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 15:09
 * @description: Instruction rearrangement test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //Initialize 4 variables
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                    c = 4;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                    d = 4;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("number of executions:"+count);
                break;
            } else {
                System.out.println("number of executions:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

In the above code, the loop starts threads A and B. If x and y are both equal to 0, the program exits. Shared variables c and d are volatile modification, equivalent to memory barriers, and count is the program count counter. The following figure is the result of printing part of the console program. It can be analyzed from the figure that when x and y are equal to 0, thread A's a = 3; x = b; the two lines of code are reordered, and in thread B, b = 3;y = a; two lines of code are also done Reorder. It is proved that the instructions above the barrier can be reordered.

3.2.2 Instructions below the barrier can be reordered.


As shown in the figure above, if the c and d barriers are placed on ordinary variables, and the code is executed again, there will still be cases where x and y are equal to 0 at the same time, which proves that the instructions under the barrier can be rearranged.

3.2.3 Instructions above and below the barrier cannot be reordered.

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 15:09
 * @description: Instruction rearrangement test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //Initialize 4 variables
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    //Disabling rearrangement up and down
                    c = 4;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    //Disabling rearrangement up and down
                    d = 4;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("number of executions:"+count);
                break;
            } else {
                System.out.println("number of executions:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

As in the above code, placing the barrier in the middle will prohibit the rearrangement of the upper and lower instructions. It is impossible for the x and y variables to be 0 at the same time. The program will always fall into an infinite loop and cannot end, which proves that the code above and below the barrier cannot be rearranged.

3.3 synchronized ordering

/**
 * @author : jiaolian
 * @date : Created in 2020-12-22 15:09
 * @description: Instruction rearrangement test
 * @modified By: 
 * Public number: call practice
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //Initialize 4 variables
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        a = 3;
                        x = b;
                    }
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        b = 3;
                        y = a;
                    }
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("number of executions:"+count);
                break;
            } else {
                System.out.println("number of executions:"+count+","+"x:"+x +" y:"+y);
            }
        }
    }
}

In the above code, x and y cannot be equal to 0 at the same time. The class object of VolatileCodeOrderTest of the synchronized lock, threads A and B are the same lock, the code is executed synchronously, and there is a sequence, so synchronized can also ensure order. sex. It is worth noting that the above code can not be synchronized with synchronized (this). This indicates that the current thread is threadA or threadB, which is not the same lock. If this test is used, x and y will be equal to 0 at the same time.

4. Programmers' learning methods

You can see that in my recent articles analyzing multithreading, I spent a lot of energy talking about visibility, atomicity and other issues, because these features are the basis for understanding multithreading. In my opinion, the foundation is very important, so how to repeat I don’t think it’s too much to write. Before that, many newbies or children’s shoes with 2 to 3 years of work experience often asked me about how to learn Java. My advice to them is to have a solid foundation and don’t come up to learn advanced knowledge points. Or frameworks, such as ReentrantLock source code, springboot framework, just like when you play games, you start playing games with higher difficulty levels. The process from entry to abandonment. At the same time, don’t just think about it when you are studying. You feel that this knowledge point will be passed by yourself. This is not enough. You need to write more code and practice more. You can deepen your understanding and memory of knowledge in the process. In fact, there are You seem to understand a lot of knowledge, but you don't practice it, and you don't really understand it. I don't recommend this method of seeing and not doing it. I have worked for 7 years after graduating from undergraduate and have been engaged in the first-line research and development of Java. , I also brought a team in the middle, because I have walked through many detours and walked over the pit, and I still have a certain experience and experience of the learning program. I will continue to organize and share some experience and knowledge in the days to come. To everyone, I hope you like to follow me. My name is to practice, and I start practicing when I call a slogan!
It can be summed up in two sentences: more hands-on, solid foundation.

5. Summary

Today, I talked to you about 3 important features of multithreading, and explained the meaning of these terms in detail in the way of code implementation. If you execute the code carefully, you should be able to understand it. If you like it, please like and follow. My name is Lian [public account], and I practice while calling.

Tags: Multithreading synchronized volatile

Posted by joejoejoe on Sat, 30 Apr 2022 00:15:00 +0300