JMM内存模型之happens-before阐述

一、happens-before的定义

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法

二、happens-before的规则

1. 程序顺序规则:

一个线程中的每个操作,happens-before于该线程中的任意后续操作。

程序顺序规则(Program Order Rule)指的是在单个线程内,按照程序的顺序,前面的操作 happens-before 后面的操作。这意味着一个线程中的每个操作都会在该线程中的任意后续操作之前发生。

下面是一个简单的Java程序示例,展示了程序顺序规则的应用:

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();
    }
}

在上述示例中,我们创建了两个线程:thread1和thread2,它们都会调用ProgramOrderDemo类中的increment()和printCount()方法。

根据程序顺序规则,线程1中的操作1(count++)happens-before 线程1中的操作2(System.out.println("Count: " + count)),同样线程2中的操作1(count++)happens-before 线程2中的操作2(System.out.println("Count: " + count))。

这意味着在每个线程中,count++操作一定会在System.out.println("Count: " + count)操作之前发生。因此,我们可以确保在每个线程中打印的count值是递增的。

请注意,尽管两个线程并行执行,但由于程序顺序规则的存在,各个线程内部的操作顺序是有序的,不会出现线程间的竞争条件。

2. 监视器锁规则:

对一个锁的解锁,happens-before于随后对这个锁的加锁。

监视器锁规则(Monitor Lock Rule)指的是对一个锁的解锁操作 happens-before 于随后对同一个锁的加锁操作。这个规则确保了在多线程环境下,对共享资源的同步访问。

下面是一个简单的Java程序示例,展示了监视器锁规则的应用:

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();
    }
}

在上述示例中,我们创建了两个线程:thread1和thread2,它们都会调用MonitorLockDemo类中的increment()和printCount()方法。在这个类中,我们使用了一个对象lock作为锁。

根据监视器锁规则,线程1中的操作1(count++)happens-before 线程2中的操作2(System.out.println("Count: " + count))。

这意味着在线程1中,对锁的解锁操作一定会在线程2中对同一个锁的加锁操作之前发生。因此,我们可以确保在执行打印操作时,count的值是线程1已经更新过的。

请注意,通过使用synchronized关键字和共享的锁对象,我们确保了对count的访问是同步的,避免了竞态条件和数据不一致的问题。这是因为监视器锁规则确保了解锁操作 happens-before 加锁操作,从而确保了对共享资源的正确同步访问。

3. volatile变量规则:

对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

volatile变量规则(Volatile Variable Rule)指的是对一个volatile域的写操作 happens-before 于任意后续对这个volatile域的读操作。这个规则确保了在多线程环境下,对volatile变量的可见性和顺序性。

下面是一个简单的Java程序示例,展示了volatile变量规则的应用:

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();
    }
}

在上述示例中,我们创建了两个线程:thread1和thread2,它们都会调用VolatileVariableDemo类中的writeNumber()和readNumber()方法。在这个类中,我们使用了一个volatile修饰的变量number。

根据volatile变量规则,线程1中的写操作(number = 42)happens-before 线程2中的读操作(System.out.println("Number: " + number))。

这意味着在线程1中,对number的写操作一定会在线程2中对同一个number的读操作之前发生。因此,我们可以确保在执行打印操作时,读取到的number的值是线程1已经写入的。

请注意,使用volatile修饰变量可以保证其在多线程环境下的可见性和顺序性。这意味着对volatile变量的写操作对其他线程是可见的,并且读操作一定会读取到最新的值。这是因为volatile变量规则确保了对volatile变量的写 happens-before 于任意后续对这个volatile变量的读。

4. 传递性:

如果A happens-before B,且B happens-before C,那么A happens-before C。

下面是一个新的示例,展示了happens-before关系的传递性:

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();
    }
}

在这个示例中,我们有一个TransitivityDemo类,其中包含一个number变量和一个ready变量。在writer()方法中,我们首先进行写操作number = 42,然后进行写操作ready = true。在reader()方法中,我们进行读操作if (ready),如果ready为true,则进行读操作System.out.println("Number: " + number)。

根据happens-before关系的传递性,线程1中的写操作number = 42 happens-before 线程1中的写操作ready = true。同时,线程1中的写操作ready = true happens-before 线程2中的读操作if (ready)。因此,我们可以得出结论,线程1中的写操作number = 42 happens-before 线程2中的读操作if (ready)。

由于happens-before关系的传递性,我们可以得出A happens-before C的结论,即线程1中的写操作number = 42 happens-before 线程2中的读操作System.out.println("Number: " + number)。

这个示例演示了happens-before关系的传递性,其中A happens-before B,B happens-before C,因此A happens-before C。

5. start()规则:

如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。

当线程A执行ThreadB.start()方法启动线程B时,根据Java内存模型(Java Memory Model,JMM)中的start()规则,线程A的ThreadB.start()操作将happens-before于线程B中的任意操作。这意味着线程B中的任意操作都可以看到线程A在调用ThreadB.start()之前的操作。

下面是一个简单的示例代码来展示这个规则:

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);
        }
    }
}

在这个示例中,线程A首先会执行一些工作,并将ready属性设置为true。然后,线程A调用threadB.start()来启动线程B。在线程B的run()方法中,我们可以看到线程A在调用ThreadB.start()之前的操作,即输出Thread B sees ready? true。

因此,根据start()规则,线程A的ThreadB.start()操作happens-before于线程B中的任意操作,确保了对ready属性的修改对线程B可见。

运行结果如下:
happens-before

6. join()规则:

如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。

根据Java内存模型(Java Memory Model,JMM)中的join()规则,如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。这意味着线程A能够看到线程B在join()之前的操作。

下面是一个简单的示例代码来展示这个规则:

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属性
        }
    }
}

在这个示例中,线程A首先会执行一些工作,并启动线程B。然后,线程A调用threadB.join()来等待线程B执行完毕。在线程B的run()方法中,我们将ready属性设置为true。

当线程A从threadB.join()成功返回后,它能够看到线程B在join()之前的操作。因此,线程A输出的Thread A sees ready? true将显示线程B在join()之前将ready属性设置为true。

因此,根据join()规则,线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回,确保了对ready属性的修改对线程A可见。

运行结果:
happens-before

猜你喜欢

转载自blog.csdn.net/qq_39939541/article/details/132350664