【Java】volatile和内存屏障


虽然synchronized可以解决原子性和内存可见性问题,但 在解决内存可见性时并没有真正通过线程间通信解决,同时 也没有解决有序性问题。所以来看一个新的关键字 volatile

内存不可见的现象

先重现一个内存可见性问题。创建两个线程,一个线程不断的循环判断标识决定是否退出,另外一个线程来修改标识位。

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

}

在这里插入图片描述

现象是当用户输入了一个非零值时,线程1并没有正确退出,出现线程不安全现象。先来刨析一下这个现象是如何产生的。
在这里插入图片描述
首先线程1在执行的过程中并没有对flag修改;在执行时,线程1先从主内存中把flag加载到自己的工作内存,也就是寄存器和缓存中;CPU对执行的过程做了一定的优化:既然当前线程没有修改变量的值,而工作内存读取速度是主内存的几万倍以上,所以每次判断flag时就从工作内存中读取;而线程2修改flag之后并没有通知线程1获取最新的值,从而导致了线程不安全现象。所以需要一种工作线程之间能相互通知的机制。

volatile

内存可见性和有序性

在上述的情况下,当为变量flag加入volatile修饰之后,就解决了内存可见性问题,程序可以正常退出。

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

⚠️ 注意:在while循环中写上sleep语句也可以达到预期效果,但是存在很大的不确定性,所以在程序中不能依赖这种存在不确定性的写法

缓存一致性协议(MESI)

先来看一下内存可见性在CPU层面是如何实现的:
在这里插入图片描述

缓存一致性协议:当某个线程修改了一个共享变量之后,通知其他CPU中该变量的缓存值为失效状态,当其他CPU中执行的指令再获取缓存变量的值时,发现这个值的状态被置为失效状态,那么就需要从主内存中重新加载最新的值

内存屏障

变量加了volatile之后,编译成指令的前后加了如下的四个屏障,其中Load表示读,Store表示写。

屏障类型 指令示例 说明
LoadLoad Load1;LoadLoad;Load2 保证load1的读操作优先于load2
StoreStore Store1;StoreStore;Store2 保证store1的写操作先于store2执行,并刷新到主内存
LoadStore Load1;LoadStore;Store2 保证load1的读操作结束先于store2的写操作
StoreLoad Store1;StoreLoad;Load2 保证store1的写操作已刷新到主内存之后,load2及其后的操作才能执行

①在每个volatile读操作之前插入LoadLoad屏障,这样就能让当前线程获取A变量的时候,保证其他线程也能获取到相同的值,这样所有线程读取到的数据就一样了。
②在每个volatile写操作之前插入StoreStore屏障,这样就能让其他线程修改A变量之后,把修改的值对当前线程可见。
③在读操作之后插入LoadStore屏障;这样就能让当前线程在其他线程修改A变量之前,获取到主内存里面A变量的值。
④在写操作之后插入StoreLoad屏障;这样就能让其他线程在获取A变量的时候,能够获取到已经被当前线程修改的值。

所以volatile可以真真正正的解决内存可见性问题,不像synchronized通过串行的方式。前面介绍过有序性是指在保证程序执行正确的前提下,编译器、CPU对指令的优化过程。用volatile修饰的变量就是要告诉编译器,不需要对这个变量涉及的操作进行优化,从而实现有序性。 所以volatile可以解决有序性问题。

原子性

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

}

上面的程序得到的结果并不如预期,原因在于:变量count被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是count++,count++的过程可以分为三步,首先获取count的值,其次对count的值进行加1,最后将得到的新值写会到缓存中。假设线程1首先得到了count的初始值100,但是还没来得及修改,就阻塞了,这时线程2开始了,它也得到了count的值,由于count的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程2得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。问题来了,线程1已经读取到了count的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程1阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存。

所以即便volatile具有可见性,但volatile也不能保证原子性。

⚠️注意:volatile只用来修饰变量,做法比较单一。


继续加油。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_43243800/article/details/130860099