线程安全性的本质volatile关键字原理

并发编程的三大特性

可见性
原子性
有序性

一个问题引发的思考

public class VolatileDemo {

    public static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int i = 0;
            
            while (flag){
                i++;
            }
        });
        
        thread.start();
        
        Thread.sleep(1000);
        
        flag = false;
    }
}

执行上面一段代码,会发现虽然把flag变量改成了false,但是线程并没有停止,貌似main线程更改了值,对于thread线程来说并不知道,这就是我们常说的线程中的可见性问题,也是引起线程安全问题的根本原因
那怎么解决这个问题呢?非常简单,java中最常见的就是通过volatile关键字解决,如下代码:

public class VolatileDemo {
    // 添加volatile关键字,解决可见性问题
    public volatile static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            int i = 0;
            
            while (flag){
                i++;
            }
        });
        
        thread.start();
        
        Thread.sleep(1000);
        
        flag = false;
    }
}

什么是可见性问题?

在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。
但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性问题

为什么会出现可见性问题呢?
硬件层面

CPU/内存/IO设备,由于运行速度的差异,所以对cpu有一定的优化
主要体现在三个方面:
1.CPU层面增加了高速缓存
2.操作系统,进程、线程、| CPU时间片来切换
3.编译器的优化 ,更合理的利用CPU的高速缓存

CPU高速缓存

因为高速缓存的存在,会导致一个缓存一致性问题。 下图是CPU高速缓存的一个模型图,我们可以分析出,ThreadA线程不能及时读取到ThreadB线程更改的值(数据不可见性),从而导致缓存数据不一致

在这里插入图片描述

什么是有序性问题?

好,我们分析了上面例子可见性的问题,再继续看下面的例子:

public class VolatileDemo02 {

    public static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        Map<String,Integer> map = new HashMap<>();
        for(int i=0;i<100000;i++){
            x=0;
            y=0;
            Thread one = new Thread(()->{
                int a = y;
                x = 1;
                map.put("a",a);
            });
            Thread two = new Thread(()->{
                int b = x;
                y = 1;
                map.put("b",b);
            });
            one.start();
            two.start();
            one.join();
            two.join();
            System.out.println("a=" + map.get("a")+", "+"b=" + map.get("b"));
        }
    }
}

打印结果按不同结果总结(可自行尝试),如下:

a=0, b=1
a=1, b=0
a=1, b=1
a=0, b=0
Process finished with exit code 0

仔细看打印结果,发现,a和b的值会有这么多不一样的情况呢?
这就是我们说的指令重排序导致的有序性问题

为什么会有指令重排序的问题呢

其实这个是CPU优化的过程中产生的一个问题
可以参考缓存一致性协议来看有序性问题的原因和解决方案,这里不再多说

volatile是怎么解决可见性和有序性的呢?

1.我们先从字节码指令层面看看添加volatile关键字前后指令有什么变化
加锁前指令:

 0x00000000027cb63e: mov     byte ptr [rsi+60h],0h  ;*putstatic flag
                                                ; - com.xufk.thread.VolatileDemo::main@24 (line 18)

加volatile关键字后指令:

0x00000000029c4687: lock add dword ptr [rsp],0h  ;*putstatic flag
                                                ; - com.xufk.thread.VolatileDemo::main@24 (line 18)

你会发现加了volatile后,字节码指令里面加了一个lock的命令,我们可以想象,这肯定跟锁有关系,没错就是通过锁实现的。

2.我们然后看volatile的在hotspot的源码实现

int field_offset = cache->f2_as_index();
     if (cache->is_volatile()) {
      if (tos_type == itos) {
       obj->release_int_field_put(field_offset, STACK_INT(-1));
     } else if (tos_type == atos) {
       VERIFY_OOP(STACK_OBJECT(-1));
       obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));      OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj>>CardTableModRefBS::card_shift], 0);
     } else if (tos_type == btos) {
       obj->release_byte_field_put(field_offset, STACK_INT(-1));
     } else if (tos_type == ltos) {
       obj->release_long_field_put(field_offset, STACK_LONG(-1));
     } else if (tos_type == ctos) {
       obj->release_char_field_put(field_offset, STACK_INT(-1));
     } else if (tos_type == stos) {
       obj->release_short_field_put(field_offset, STACK_INT(-1));
     } else if (tos_type == ftos) {
       obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
     } else {
       obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
     }
      OrderAccess::storeload();
    }

我们可以看到 OrderAccess::storeload();这句代码,这是加了内存屏障实现的,解决指令重排序问题

其实volatile的实现原理,必须要先去学习Java内存模型(JMM),了解清楚多线程共享内存和工作内存之间数据是怎样交互的之后,就会对原理更加清楚了

总结

可见性:总线锁和缓存锁,缓存锁是基于缓存一致性协议来实现。
有序性:内存屏障指令

volatile解决了并发编程的可见性和有序性问题,但是并没有解决原子性问题,原子性问题还得以来synchronize这种锁来实现

相关链接:
Java内存模型(JMM)
缓存一致性协议
内存屏障

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:1354518382@qq.com 博客地址:https://blog.csdn.net/qq_35576976/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值