[Java] volatile and memory barriers


Although synchronized can solve the problem of atomicity and memory visibility, it does not really solve the problem of inter-thread communication when solving memory visibility , and it does not solve the problem of order . So let's look at a new keyword volatile .

Phenomena that are not visible in memory

First reproduce a memory visibility problem. Create two threads, one thread continuously loops to judge the flag to decide whether to exit, and the other thread modifies the flag bit.

public class Demo01_Volatile {
    
    
    private static int flag = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个线程
        Thread t1 = new Thread(() -> {
    
    
            System.out.println("t1线程已启动..");
            //循环判断标识位
            while (flag == 0){
    
    
                //TODO:
            }
            System.out.println("t1线程已退出..");
             
        });
        //启动线程
        t1.start();

        //定义第二个线程,来修改flag的值
        Thread t2 = new Thread(() -> {
    
    
            System.out.println("t2线程已启动..");
            System.out.println("请输入一个整数:");
            Scanner scanner = new Scanner(System.in);
            //接收用户输入并修改flag的值
            flag = scanner.nextInt();
            System.out.println("t2线程已退出..");
        });
        //确保t1先启动
        TimeUnit.SECONDS.sleep(1);
        //启动线程
        t2.start();
    }

}

insert image description here

The phenomenon is that when the user enters a non-zero value, thread 1 does not exit correctly , and the thread is not safe. Let’s first analyze how this phenomenon arises.
insert image description here
First of all, thread 1 did not modify the flag during execution; during execution, thread 1 first loaded the flag from the main memory into its own working memory, that is, registers and caches; the CPU made certain changes to the execution process. Optimization: Since the current thread does not modify the value of the variable, and the reading speed of the working memory is tens of thousands of times that of the main memory, it reads from the working memory every time the flag is judged; and thread 2 does not notify the thread after modifying the flag 1 Get the latest value, which leads to thread insecurity. Therefore, a mechanism for mutual notification between worker threads is needed.

volatile

memory visibility and ordering

In the above case, after adding volatile modification to the variable flag, the problem of memory visibility is solved, and the program can exit normally.

public class Demo01_Volatile {
    
    
    //注意观看volatile修饰后的现象
    private static volatile int flag = 0;
    public static void main(String[] args) throws InterruptedException {
    
    
        //定义一个线程
        Thread t1 = new Thread(() -> {
    
    
            System.out.println("t1线程已启动..");
            //循环判断标识位
            while (flag == 0){
    
    
                //TODO:
            }
            System.out.println("t1线程已退出..");
             
        });
        //启动线程
        t1.start();

        //定义第二个线程,来修改flag的值
        Thread t2 = new Thread(() -> {
    
    
            System.out.println("t2线程已启动..");
            System.out.println("请输入一个整数:");
            Scanner scanner = new Scanner(System.in);
            //接收用户输入并修改flag的值
            flag = scanner.nextInt();
            System.out.println("t2线程已退出..");
        });
        //确保t1先启动
        TimeUnit.SECONDS.sleep(1);
        //启动线程
        t2.start();
    }
}

⚠️ Note: Writing the sleep statement in the while loop can also achieve the desired effect, but there is a lot of uncertainty, so you can't rely on this uncertain writing in the program .

Cache Coherence Protocol (MESI)

Let's take a look at how memory visibility is implemented at the CPU level:
insert image description here

Cache consistency protocol: When a thread modifies a shared variable, it notifies other CPUs that the cache value of the variable is invalid. If it is invalidated, the latest value needs to be reloaded from main memory .

memory barrier

After adding volatile to the variable , the following four barriers are added before and after compiling instructions , where Load means reading and Store means writing.

barrier type command example illustrate
LoadLoad Load1;LoadLoad;Load2 Ensure that the read operation of load1 takes precedence over load2
StoreStore Store1;StoreStore;Store2 Ensure that the write operation of store1 is executed before store2 and flushed to the main memory
LoadStore Load1;LoadStore;Store2 Ensure that the read operation of load1 ends before the write operation of store2
StoreLoad Store1;StoreLoad;Load2 Ensure that the write operation of store1 has been flushed to the main memory before load2 and subsequent operations can be executed

① Insert a LoadLoad barrier before each volatile read operation, so that when the current thread obtains the A variable, other threads can also obtain the same value, so that the data read by all threads is the same.
② Insert a StoreStore barrier before each volatile write operation, so that other threads can modify the A variable and make the modified value visible to the current thread.
③ Insert the LoadStore barrier after the read operation; in this way, the current thread can obtain the value of the A variable in the main memory before other threads modify the A variable.
④ Insert the StoreLoad barrier after the write operation; this will allow other threads to obtain the value that has been modified by the current thread when obtaining the A variable.

So volatile can really solve the problem of memory visibility, unlike synchronized through serial. As mentioned above, the orderliness refers to the optimization process of instructions by the compiler and CPU under the premise of ensuring the correct execution of the program. The variable modified with volatile is to tell the compiler that there is no need to optimize the operations involved in this variable, so as to achieve order . So volatile can solve the order problem.

atomicity

public class Demo02_Volatile {
    
    

    // 定义自增操作的对象
    private static Counter2 counter = new Counter2();

    public static void main(String[] args) throws InterruptedException {
    
    
        // 定义两个线程,分别自增5万次
        Thread t1 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // 调用加锁的方法
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
    
    
            for (int i = 0; i < 50000; i++) {
    
    
                // 调用没有加锁的方法
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();
        // 等待自增完成
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("count = " + counter.count);

    }
}

class Counter2 {
    
    
    public static volatile  int count = 0;

    // 修饰静态方法
    public static void increment() {
    
    
        // 要执行的修改逻辑
        count++;
    }

}

The result obtained by the above program is not as expected. The reason is that the variable count is modified by volatile. If two threads want to modify this variable, they both perform an auto-increment operation on it, that is, count++. The process of count++ can be divided into three steps. First, obtain The value of count, then add 1 to the value of count, and finally write the new value to the cache . Assume that thread 1 first got the initial value of count 100, but it was blocked before it could be modified. At this time, thread 2 started, and it also got the value of count. Since the value of count has not been modified, even if it is modified by volatile , the variable in the main memory has not changed, then the value obtained by thread 2 is also 100, and then add 1 to it, and after getting 101, write the new value into the cache, and then flush it into the main memory. According to the principle of visibility, the value of this main memory can be seen by other threads. Here comes the problem. Thread 1 has read the value of count as 100, which means that the atomic operation read has ended, so this visibility comes a bit late. After thread 1 is blocked, continue to add the value of 100 to 1. Get 101, then write the value to the cache, and finally flush it into the main memory.

So even though volatile has visibility, volatile cannot guarantee atomicity.

⚠️ Note: volatile is only used to modify variables, and the method is relatively simple.


Keep going.
insert image description here

Guess you like

Origin blog.csdn.net/qq_43243800/article/details/130860099