Three major features of concurrent programming: 2. Visibility (what is visibility and how to solve it)

Table of contents

2.1 What is visibility

2.2 Ways to solve visibility


2.1 What is visibility

The visibility problem is based on the location of the CPU. The processing speed of the CPU is very fast. Compared with the CPU, it is too slow to go to the main memory to obtain data. The CPU provides L1, L2, and L3 three-level caches. After the memory gets the data, it will be stored in the CPU's L3 cache. Every time it goes to the L3 cache to get the data, the efficiency will definitely improve.

This brings problems. Now that the CPU is multi-core, the working memory (CPU L3 cache) of each thread is independent. When making changes in each thread, it will only change its own working memory, and there is no timely Synchronized to main memory, resulting in data inconsistency.

Code logic for visibility issues

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (flag) {
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}

2.2 Ways to solve visibility

2.2.1 volatile

volatile is a keyword used to modify member variables.

If the attribute is modified by volatile, it is equivalent to telling the CPU that the operation of the current attribute is not allowed to use the CPU cache, and must be operated with the main memory

The memory semantics of volatile:

  • The volatile attribute is written: When writing a volatile variable, JMM will refresh the CPU cache corresponding to the current thread to the main memory in time
  • The volatile attribute is read: When reading a volatile variable, JMM will invalidate the memory in the corresponding CPU cache, and must go to the main memory to re-read the shared variable

In fact, the addition of volatile is to inform the CPU that the CPU cache is not allowed for the read and write operations of the current attribute. The attribute modified by volatile will be converted to assembly, and a lock prefix will be added. When the CPU executes this instruction, if Prefixing it with lock does two things:

  • Write the data of the current processor cache line back to main memory
  • The data written back is directly invalid in the caches of other CPU cores.

Summary: volatile means that every time the CPU operates on this data, it must immediately synchronize to the main memory and read data from the main memory.

private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (flag) {
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}

2.2.2 synchronized

Synchronized can also solve the visibility problem, synchronized memory semantics.

If a synchronized synchronization code block or a synchronization method is involved, after the lock resource is acquired, the variables involved in it are removed from the CPU cache, and the data must be retrieved from the main memory, and the CPU will be immediately reset after the lock is released. The data in the cache is synchronized to the main memory.

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (flag) {
            synchronized (MiTest.class){
                //...
            }
            System.out.println(111);
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}

2.2.3 Lock

The way Lock locks ensure visibility is completely different from synchronized. Based on its memory semantics, synchronized performs an operation of synchronizing the CPU cache to the main memory when acquiring and releasing locks.

Lock locks are implemented based on volatile. When locking and releasing the lock inside the Lock lock, a state attribute modified by volatile will be added or subtracted.

If the volatile modified attribute is written, the CPU will execute the instruction prefixed with lock, and the CPU will immediately synchronize the modified data from the CPU cache to the main memory, and also synchronize other attributes to the main memory immediately. . It also invalidates this data in other CPU cache lines and must be re-pulled from main memory.

private static boolean flag = true;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        while (flag) {
            lock.lock();
            try{
                //...
            }finally {
                lock.unlock();
            }
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}

2.2.4 final

Attributes modified by final are not allowed to be modified during runtime. In this way, the visibility is indirectly guaranteed. All multi-threaded read final attributes must have the same value.

Final does not mean that every time data is read from the main memory, it is not necessary, and final and volatile are not allowed to modify a property at the same time

The content modified by final is no longer allowed to be written again, and volatile is to ensure that every read and write data is read from the main memory, and volatile will affect certain performance, so there is no need to modify it at the same time.

Guess you like

Origin blog.csdn.net/weixin_42717648/article/details/130727625