通过demo对比了解Volatile的实现

本文探讨了volatile关键字在Java中的作用,它确保了共享变量的线程可见性和防止指令重排,但无法保证原子性。通过一个线程加法的例子解释了volatile如何保证可见性但无法解决原子性问题,同时也提到了内存屏障的概念以及其在防止指令重排中的应用。此外,还提及了volatile在单例模式双重检查锁定中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


Volatile的定义

  volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。

  上面的定义有两层含义:
  1,保证共享变量的可见性,但无法保证原子性
  2,防止指令重排

Volatile的可见性

  一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
  
  可见性对于共享变量在多CPU并行执行程序尽量保证不出错的一个很重要的特性。
  
  因为线程所执行需要的数据都在主存中,而对数据的操作是由CPU来完成的,每次CPU需要数据都去主存中取,这样是很不科学的,CPU执行的速度很快,而主存完全跟不上节奏。所以每个CPU都有自己私有的高速缓存区,在执行操作之前,将需要的数据从内存中复制到CPU高速缓存区,计算完毕后,将结果重新写回主存。
  单线程操作时,不会发生问题,但是在多线程的情况下就不一样了。

我们来一个小demo

public class IncrVolatileVal {

    public static int i =0;
    static class MyThread{
        public static void start() {
           Thread t = new Thread(()->{
               for(int j = 0 ; j<1000;j++){
                   i++;
               }
           });
           t.start();
        }
    }

    public static void main(String[] args) {
        for(int i =0 ;i<5;i++){
            IncrVolatileVal.MyThread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while("main".equals(Thread.currentThread().getName())) {
            System.out.println(IncrVolatileVal.i);
            break;
        }
    }
}

这是一个每个线程为 i 变量加1000次,5个线程同时工作的小程序,但是我们取的最后结果总是小于5000。

我们可以大致模拟小于的情况
  A线程先从主存取出 i = 8并且复制到自己的高速缓冲区的时候,有个B线程也从主存读取到了8,但是它成功将9写入主存,但是A线程还是以为8是最新值,做着加1操作,所以A线程的这次任务提交失败。所以导致加完之后的总数小于5000。
  这是因为 i 的值被更新了,但是其它线程操作的值还是自己高速缓冲区的值 。
  volatile定义是保证共享变量在内存的可见性,就是共享变量被其它线程更新了值,其它的高速缓冲区能立马知道变化,并且更新自己缓冲区的值。那是不是我们将 i 用volatile修饰就能得到结果呢?
  

我们来一个测试volatile修饰共享变量的小demo

public class IncrVolatileVal {

    public static volatile int i =0;
    static class MyThread{
        public static void start() {
           Thread t = new Thread(()->{
               for(int j = 0 ; j<1000;j++){
                   i++;
               }
           });
           t.start();
        }
    }

    public static void main(String[] args) {
        for(int i =0 ;i<5;i++){
            IncrVolatileVal.MyThread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while("main".equals(Thread.currentThread().getName())) {
            System.out.println(IncrVolatileVal.i);
            break;
        }
    }
}

但是结果还是小于5000是怎么回事呢?

还是分析一下:

  还是A线程取到 i 的值为8,但是这是B线程修改了 i 现在为9了。由于 i 是volatile 修饰,A线程将自己的高速缓存更新为 i = 9。然后开始对 i 进行运算,但是还是会出现提交失败,A 取到的9是当时的最新值,但是 i++的过程是,先取 i 的值,在将 i 的值加1,在将 i+1的结果赋值给 i 。然后问题就出现在这里,这是3个原子操作,但是合在一起就并不是原子操作了。在某一步,可能其它线程把值给改了,然后导致这次任务提交失败。
  这次失败是因为 i++操作 并不是原子操作。

这两个例子对比,可以体现出了volatile保证共享变量的可见性,但无法保证原子性

Volatile的防止指令重排

在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

1,编译器重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2,处理器重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

看的书都有介绍下面语句:
  volatile防止指令重排是通过内存屏障实现的,被volatile修饰的变量,在汇编语句中会有个lock前缀。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。

然后我准备了单例模式的双重校验锁实现做demo,那个volatile起到防止重排的作用。

 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 

通过jvm参数来看汇编指令,然后 给我报了个

Java HotSpot(TM) 64-Bit Server VM warning: PrintAssembly is enabled; turning on DebugNonSafepoints to gain additional output
Could not load hsdis-amd64.dll; library not loadable; PrintAssembly is disabled

差了一个插件,过几天在配。现在还要抓紧继续下去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值