Visibility and Order of JUC

Table of contents

java memory model

visibility

Phenomena appear 

phenomenon explanation 

Solution

orderliness

weird results

Solution

Happens-before rule


java memory model

The Java Memory Model (Java Memory Model, JMM for short) defines the access methods and memory relationships of various variables and objects in a Java program. JMM specifies issues such as visibility, atomicity, and sequence between threads to ensure code correctness during concurrent access by multiple threads.

The main concepts in JMM include:

  1. main memory and working memory

    The main memory is a cache in Java's memory model, a storage area for shared variables, and all threads can access it.

    Working memory is each thread's private memory, which holds copies of variables needed by the thread to execute. The read and write operations of threads to shared variables are all performed in the working memory, and threads cannot directly read and write each other's working memory.

  2. atomicity

    In JMM, atomicity means that an operation is an indivisible whole, either all execution succeeds or all execution fails. JMM guarantees that the read and assignment operations of a single variable are atomic. If you want to implement atomic operations on multiple variables, you need to lock or use atomic classes.

  3. visibility

    Visibility means that variables modified by one thread are visible to other threads. In JMM, there is no guarantee that after one thread modifies a variable, another thread can see the change immediately. This is because each thread has its own working memory, and threads cannot directly read and write each other's working memory. If you need to ensure visibility, you can use the volatile keyword or lock synchronization.

  4. orderliness

    Order means that the order in which the program is executed is consistent with the order in which the code is written. In JMM, there is no guarantee that the execution order of the program is exactly the same as the order in which the code is written, but it will try to ensure that the results of the program execution meet expectations through various means.

JVM regulates the behavior of multi-threaded concurrent execution by constraining JMM, so as to ensure the correctness of Java programs. Although JMM is a model, the JVM will optimize it when it is implemented, so developers need to pay attention to writing thread-safe, correct, and efficient programs.

JMM is reflected in the following aspects

  • Atomicity - guarantees that instructions will not be affected by thread context switches
  • Visibility - ensures that instructions are not affected by the cpu cache
  • Orderedness - guarantees that instructions will not be affected by parallel optimization of cpu instructions

visibility

Phenomena appear 

Let's look at a phenomenon first. The modification of the run variable by the main thread is invisible to the t thread, which makes the t thread unable to stop:  

public class ThreadText  {
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {

        ThreadText t = new ThreadText();

        new Thread(() -> {
            System.out.println("线程启动了");
            while (run) {
//                 System.out.println("线程进行中");
            }
            System.out.println("线程即将结束了");
        }).start();

        Thread.sleep(100);

        run = false;
        System.out.println("线程状态发生改变");
    }
    
}

 Phenomenon 1: If you run the above code directly, the newly created thread will not stop.

 Phenomenon 2: If you open the code I commented, the newly created thread will stop

phenomenon explanation 

why? Analyze it:

1. In the initial state, the t thread has just read the value of run from the main memory to the working memory.

2. Because the t thread frequently reads the value of run from the main memory, the JIT compiler will cache the value of run to the cache in its own working memory, reducing the access to run in the main memory and improving efficiency.

3. After 1 second, the main thread modifies the value of run and synchronizes it to the main memory, while t reads the value of this variable from the cache in its own working memory, and the result is always the old value

Solution

volatile (volatile keyword) It can be used to modify member variables and static member variables. It can prevent threads from looking up the value of variables from their own working cache. It must go to main memory to obtain its value. Thread operations on volatile variables are direct access to main memory

Visibility vs Atomicity

The actual example reflected in the previous example is visibility, which ensures that among multiple threads, the modification of a volatile variable by one thread is visible to another thread, and atomicity cannot be guaranteed. It is only used in one writing thread and multiple reading threads. Situation: The above example is understood from the bytecode as follows:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次 
getstatic run // 线程 t 获取 run false 

Two threads, one i++ and one i--, can only guarantee to see the latest value, and cannot solve instruction interleaving

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 
 
getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 
 
iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1 

Note that the synchronized statement block can not only ensure the atomicity of the code block, but also ensure the visibility of the variables in the code block. But the downside is

Synchronized is a heavyweight operation with relatively lower performance. If you add System.out.println() to the infinite loop in the previous example, you will find that even without the volatile modifier, thread t can correctly see the modification of the run variable. Think about why?

System.out.println caused thread synchronization

public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }


Although there is no volatile to guarantee the visibility of shared data under multi-threading, there are still other constraints to ensure data consistency in the happens-before principle in JMM (Java Memory Model).

orderliness

The JVM can adjust the execution order of the statements without affecting the correctness. Consider the following piece of code

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

 It can be seen that whether to execute i or j first will not affect the final result. Therefore, when the above code is actually executed, it can be either

i = ...; 
j = ...;

can also be

j = ...;
i = ...; 

This feature is called "instruction rearrangement", and "instruction rearrangement" under multi-threading will affect the correctness. Why is there an optimization for rearranging instructions? Let's understand from the principle of CPU executing instructions

weird results

    int num = 0;
    boolean ready = false;
    // 线程1 执行此方法
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    // 线程2 执行此方法
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

I_Result is an object with an attribute r1 used to save the result. Ask, how many possible results are there?

Case 1: Thread 1 executes first, then ready = false, so the result of entering the else branch is 1

Situation 2: Thread 2 executes num = 2 first, but does not have time to execute ready = true, thread 1 executes, or enters the else branch, and the result is 1

Case 3: Thread 2 executes until ready = true, thread 1 executes, this time enters the if branch, and the result is 4 (because num has already been executed)

The result may also be 0

In this case: thread 2 executes ready = true, switches to thread 1, enters the if branch, adds up to 0, then switches back to thread 2 and executes num = 2

This phenomenon is called instruction rearrangement, which is some optimization of the JIT compiler at runtime. This phenomenon requires a lot of tests to reproduce.

Solution

Variables modified by volatile can disable instruction rearrangement.

When a variable is declared volatile, both the compiler and the runtime are restricted from reordering the variable. Specifically, both read and write operations on volatile variables pass through memory barriers to ensure their order and visibility.

A memory barrier is a set of CPU instructions that prevents the processor from reordering memory accesses and guarantees the execution order of certain instructions. For read operations, the memory barrier will force the CPU to flush all previous data-modifying instructions back to the main memory before executing the read instruction; for write operations, the memory barrier will cause the CPU to force the data to be written to the main memory after executing the write instruction. live.

The Happens-before principle is a concept in the Java memory model, which defines the visibility relationship between write operations and read operations on shared variables under concurrent conditions, including thread startup, thread termination, synchronization blocks, volatile variables, etc. Various scenarios ensure the order of operations among multiple threads.

Happens-before rule

Happens-before stipulates that the write operation of the shared variable is visible to the read operation of other threads. It is a set of rule summary of visibility and order. Aside from the happens- before rule, JMM cannot guarantee that a thread can read the shared variable. Write, visible to other threads reading the shared variable

Specifically, if operation A happens-before operation B, then we are guaranteed that modifications to shared variables made by threads that see operation A are visible to threads that operate B. In other words, the happens-before principle stipulates some specifications for the execution order of operations between different threads, which can avoid some problems caused by concurrency.

Here are a few important happens-before rules:

  1. The sequence rules of the program: each operation in a thread is executed in the order of the program code

  2. Volatile variable rule: A write operation to a volatile variable precedes a subsequent read operation on the variable

  3. Transitivity: If operation A happens-before operation B, operation B happens-before operation C, then operation A happens-before operation C

  4. Synchronized rule: For the same lock, the unlocking operation of the lock happens-before the subsequent locking operation of the lock

  5. Thread start rules: A thread's start() method happens-before any operation of the thread

  6. Thread termination rule: all operations of a thread happen-before other threads detect that the thread has terminated

  7. Object constructor rules: An object's constructor executes (happens-before) its finalize() method

The Happens-before principle is an important basis for Java to implement multi-threaded operations. When understanding and analyzing multi-threaded programs, these rules need to be followed and applied to avoid thread safety issues.

Guess you like

Origin blog.csdn.net/m0_62436868/article/details/131199804