JMM memory model happens-before explanation

1. Definition of happens-before

  • If one operation happens-before another operation, then the execution results of the first operation will be visible to the second operation, and the execution order of the first operation will be before the second operation.
  • The existence of a happens-before relationship between two operations does not mean that the specific implementation of the Java platform must be executed in the order specified by the happens-before relationship. If the execution result after reordering is consistent with the execution result according to the happens-before relationship, then this reordering is not illegal.

2. Rules of happens-before

1. Program sequence rules:

Each operation in a thread happens-before any subsequent operation in that thread.

Program Order Rule refers to within a single thread, according to the order of the program, the previous operation happens-before the following operation. This means that every operation in a thread occurs before any subsequent operation in that thread.

The following is a simple Java program example that demonstrates the application of program sequence rules:

public class ProgramOrderDemo {
    
    
    private int count = 0;

    public void increment() {
    
    
        count++; // 操作1
    }

    public void printCount() {
    
    
        System.out.println("Count: " + count); // 操作2
    }

    public static void main(String[] args) {
    
    
        ProgramOrderDemo demo = new ProgramOrderDemo();

        Thread thread1 = new Thread(() -> {
    
    
            demo.increment(); // 线程1中的操作1
            demo.printCount(); // 线程1中的操作2
        });

        Thread thread2 = new Thread(() -> {
    
    
            demo.increment(); // 线程2中的操作1
            demo.printCount(); // 线程2中的操作2
        });

        thread1.start();
        thread2.start();
    }
}

In the above example, we created two threads: thread1 and thread2, both of which call the increment() and printCount() methods in the ProgramOrderDemo class.

According to the program sequence rules, operation 1 (count++) in thread 1 happens-before operation 2 (System.out.println("Count: " + count)) in thread 1, and similarly operation 1 (count++) in thread 2 happens-before -before operation 2 in thread 2 (System.out.println("Count: " + count)).

This means that in each thread, the count++ operation must occur before the System.out.println("Count: " + count) operation. Therefore, we can ensure that the count value printed in each thread is incremented.

Please note that although the two threads execute in parallel, due to the existence of program sequence rules, the order of operations within each thread is orderly, and no race conditions between threads will occur.

2. Monitor lock rules:

The unlocking of a lock happens-before the subsequent locking of the lock.

Monitor Lock Rule means that the unlocking operation of a lock happens-before the subsequent locking operation of the same lock. This rule ensures synchronized access to shared resources in a multi-threaded environment.

The following is a simple Java program example showing the application of monitor lock rules:

public class MonitorLockDemo {
    
    
    private int count = 0;
    private Object lock = new Object();

    public void increment() {
    
    
        synchronized (lock) {
    
    
            count++; // 线程1中的操作1
        }
    }

    public void printCount() {
    
    
        synchronized (lock) {
    
    
            System.out.println("Count: " + count); // 线程2中的操作2
        }
    }

    public static void main(String[] args) {
    
    
        MonitorLockDemo demo = new MonitorLockDemo();

        Thread thread1 = new Thread(() -> {
    
    
            demo.increment(); // 线程1中的操作1
        });

        Thread thread2 = new Thread(() -> {
    
    
            demo.printCount(); // 线程2中的操作2
        });

        thread1.start();
        thread2.start();
    }
}

In the above example, we created two threads: thread1 and thread2, both of which call the increment() and printCount() methods in the MonitorLockDemo class. In this class, we use an object lock as the lock.

According to the monitor lock rules, operation 1 (count++) in thread 1 happens-before operation 2 (System.out.println("Count: " + count)) in thread 2.

This means that in thread 1, the unlocking operation of the lock must occur before the locking operation of the same lock in thread 2. Therefore, we can ensure that the value of count has been updated by thread 1 when performing the print operation.

Please note that by using the synchronized keyword and a shared lock object, we ensure that access to count is synchronized, avoiding race conditions and data inconsistencies. This is because the monitor lock rules ensure that the unlocking operation happens-before the locking operation, thus ensuring correct synchronized access to the shared resource.

3. Volatile variable rules:

A write to a volatile field happens-before any subsequent read from the volatile field.

Volatile Variable Rule means that a write operation to a volatile field happens-before any subsequent read operation to this volatile field. This rule ensures the visibility and ordering of volatile variables in a multi-threaded environment.

The following is a simple Java program example showing the application of volatile variable rules:

public class VolatileVariableDemo {
    
    
    private volatile int number = 0;

    public void writeNumber() {
    
    
        number = 42; // 写操作
    }

    public void readNumber() {
    
    
        System.out.println("Number: " + number); // 读操作
    }

    public static void main(String[] args) {
    
    
        VolatileVariableDemo demo = new VolatileVariableDemo();

        Thread thread1 = new Thread(() -> {
    
    
            demo.writeNumber(); // 写操作
        });

        Thread thread2 = new Thread(() -> {
    
    
            demo.readNumber(); // 读操作
        });

        thread1.start();
        thread2.start();
    }
}

In the above example, we created two threads: thread1 and thread2, both of which call the writeNumber() and readNumber() methods in the VolatileVariableDemo class. In this class, we use a volatile modified variable number.

According to the volatile variable rules, the write operation in thread 1 (number = 42) happens-before the read operation in thread 2 (System.out.println("Number: " + number)).

This means that the write operation on number in thread 1 must occur before the read operation on the same number in thread 2. Therefore, we can ensure that when performing a print operation, the value of number read is what thread 1 has written.

Please note that using volatile to modify variables can ensure their visibility and order in a multi-threaded environment. This means that writes to volatile variables are visible to other threads, and reads will always read the latest value. This is because the volatile variable rules ensure that writes to a volatile variable happen-before any subsequent reads of that volatile variable.

4. Transitivity:

If A happens-before B, and B happens-before C, then A happens-before C.

Here is a new example demonstrating the transitive nature of happens-before relationships:

public class TransitivityDemo {
    
    
    private int number = 0;
    private volatile boolean ready = false;

    public void writer() {
    
    
        number = 42; // 写操作
        ready = true; // 写操作
    }

    public void reader() {
    
    
        if (ready) {
    
     // 读操作
            System.out.println("Number: " + number); // 读操作
        }
    }

    public static void main(String[] args) {
    
    
        TransitivityDemo demo = new TransitivityDemo();

        Thread thread1 = new Thread(() -> {
    
    
            demo.writer(); // 写操作
        });

        Thread thread2 = new Thread(() -> {
    
    
            demo.reader(); // 读操作
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we have a TransitivityDemo class which contains a number variable and a ready variable. In the writer() method, we first perform the write operation number = 42, and then perform the write operation ready = true. In the reader() method, we perform the reading operation if (ready). If ready is true, the reading operation System.out.println("Number: " + number) is performed.

According to the transitivity of the happens-before relationship, the write operation number = 42 in thread 1 happens-before the write operation ready = true in thread 1. At the same time, the write operation in thread 1 ready = true happens-before the read operation in thread 2 if (ready). Therefore, we can conclude that the write operation number = 42 in thread 1 happens-before the read operation in thread 2 if (ready).

Due to the transitive nature of the happens-before relationship, we can conclude that A happens-before C, that is, the write operation number = 42 in thread 1 happens-before the read operation in thread 2 System.out.println("Number: " + number).

This example demonstrates transitivity of the happens-before relation, where A happens-before B, B happens-before C, therefore A happens-before C.

5. start() rules:

If thread A performs the operation ThreadB.start() (starts thread B), then the ThreadB.start() operation of thread A happens-before any operation in thread B.

When thread A executes the ThreadB.start() method to start thread B, according to the start() rule in the Java Memory Model (JMM), the ThreadB.start() operation of thread A will happen-before in thread B. any operation. This means that any operation in thread B can see the operation of thread A before calling ThreadB.start().

Here is a simple example code demonstrating this rule:

public class ThreadDemo {
    
    
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        ThreadB threadB = new ThreadB();
        Thread threadA = new Thread(() -> {
    
    
            System.out.println("Thread A is doing some work");
            ready = true;
            threadB.start(); // 线程A启动线程B
        });

        threadA.start(); // 启动线程A
        threadA.join(); // 等待线程A执行完毕
        System.out.println("Thread B is ready? " + threadB.isReady());
    }

    static class ThreadB extends Thread {
    
    
        public boolean isReady() {
    
    
            return ready;
        }

        @Override
        public void run() {
    
    
            System.out.println("Thread B is running");
            // 在这里可以看到线程A在调用ThreadB.start()之前的操作
            System.out.println("Thread B sees ready? " + ready);
        }
    }
}

In this example, thread A will first perform some work and set the ready attribute to true. Then, thread A calls threadB.start() to start thread B. In the run() method of thread B, we can see the operation of thread A before calling ThreadB.start(), that is, the output Thread B sees ready? true.

Therefore, according to the start() rule, the ThreadB.start() operation of thread A happens-before any operation in thread B, ensuring that modifications to the ready attribute are visible to thread B.

The running results are as follows:
happens-before

6. join() rules:

If thread A performs the ThreadB.join() operation and returns successfully, then any operation in thread B happens-before thread A successfully returns from the ThreadB.join() operation.

According to the join() rule in the Java Memory Model (JMM), if thread A executes the operation ThreadB.join() and returns successfully, then any operation in thread B happens-before thread A returns from ThreadB.join( ) operation returns successfully. This means that thread A can see the operations of thread B before join().

Here is a simple example code demonstrating this rule:

public class ThreadDemo {
    
    
    private static boolean ready = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        ThreadB threadB = new ThreadB();
        Thread threadA = new Thread(() -> {
    
    
            System.out.println("Thread A is doing some work");
            threadB.start(); // 线程A启动线程B
            try {
    
    
                threadB.join(); // 线程A等待线程B执行完毕
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("Thread A sees ready? " + ready);
        });

        threadA.start(); // 启动线程A
        threadA.join(); // 等待线程A执行完毕
        System.out.println("Thread B is ready? " + threadB.isReady());
    }

    static class ThreadB extends Thread {
    
    
        public boolean isReady() {
    
    
            return ready;
        }

        @Override
        public void run() {
    
    
            System.out.println("Thread B is running");
            ready = true; // 在这里修改ready属性
        }
    }
}

In this example, thread A will first perform some work and start thread B. Then, thread A calls threadB.join() to wait for thread B to complete execution. In the run() method of thread B, we set the ready attribute to true.

When thread A returns successfully from threadB.join(), it can see the operations of thread B before join(). Therefore, Thread A sees ready? true output by Thread A will show that Thread B set the ready attribute to true before join().

Therefore, according to the join() rule, any operation in thread B happens-before thread A successfully returns from the ThreadB.join() operation, ensuring that modifications to the ready attribute are visible to thread A.

operation result:
happens-before

Guess you like

Origin blog.csdn.net/qq_39939541/article/details/132350664