九、Volatile

目录

Volatile

保证可见性

禁止重排序

CPU的乱序执行

单例


Volatile

volatile 可以说是 JVM 实现的一种轻量级的同步机制,是针对某种场景下的效率优化。

也就是并不是用了 volatile 就一定能实现线程安全,而是在一定条件下用 volatile 才能实现线程安全。在这些情况下使用 volatile,代价比较低,效率比较高

Volatile 并不能保证多个线程共同修改变量值带来的一致性问题 不能保证原子性

当volatile开始修饰一个变量的时候,代表

  • 这个变量线程间可见
  • 禁止重排序

保证可见性

保证可见性指的是:当一个线程修改了某个变量时,其他所有线程都知道该变量被修改了。 由于 volatile 可以保证可见性,因此 Java 能够保证现在在读取 volatile 变量时,线程读取到的值是准确的。但是这并不意味着对 volatile 变量的操作是线程安全的,因为有可能在读取到变量之后,又有其他线程对变量进行修改了。因为在CPU乱序执行的情况下,CPU读取到某个变量的值并且执行运算逻辑单元指令的期间,可能其他线程进来执行自增操作,然后当前现在在写入到内存的时候会发生数据覆盖,导致数据不一致。

要使得volatile不发生并发安全问题,只需要遵守如下两条规则即可:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

在多线程递增的变量count  其运算结果就依赖于变量的当前值,使用count ++ 最终都会依赖当前变量的值如果改为 count  = 1,这样的情况,那么 count  的值就不依赖变量当前值,因此就不会有线程安全问题

  • 变量不需要与其他的状态变量共同参与不变约束。

其意思是说,变量不能和其他变量一起参与判断,无论其他变量是否是 volatile 类型的变量。例如 if(a && b) 这个判断就无法满足 volatile 的第二条规则,会发生线程安全问题,即使这两个变量都是 volatile 类型的变量。

禁止重排序

禁止重排序的实现,是使用了一个叫「内存屏障」的东西。简单地说,内存屏障的作用就是指令重排序时,不能把后面的指令重排序到内存屏障之前的位置。

CPU的乱序执行

CPU速度特别快,他比内存快100个数量级,比硬盘快100万。

指令1与指令2两者没有依赖关系,CPU为了提高效率,可能原先执行指令1然后在执行指令2变为先执行指令2在执行指令1,这个就是执行的重排序。
volatile禁止指令重排序

单例

  • new一个对象,并且他的构造方法是私有的,他有个public方法,外部调用不能new这个对象,只能调用getInstance方法,返回我们new的对象,这时候调用方法返回的对象都是同一个,同一个引用,下面就是饿汉示单例。

  • 程序改造,能不能让他在使用的时候在开始创建对象,不用提前创建好,防止浪费资源,先判断INSTANCE是否为空,不为空直接拿来用。这个方法不能保证线程安全,在多线程的情况会出现上一个线程还在睡眠状态,下一个线程又开始进行执行创建对象,就会导致所以的对象不是同一个对象。
  • 解决办法,线程同步机制,上锁,线程一致性,如果在getInstance里面还有业务逻辑时候锁的粒度太粗了,所以要把锁的粒度变细 
  •  解决办法:把锁的粒度变细,这种方法还是有问题的,第一个线程拿到锁然后线程睡眠了,这时候第二个线程再次判断为null,然后往下面执行创建对象,这时候第二个线程拿到锁进行执行创建对象,会导致创建的对象不是同一个。

  • 最终诞生了DCL Double Check Loading
  • 先判断是否为空,为空进行上锁,再次判断是否为空,依然为空就说明没有一个线程改过,没有线程改动过就进行创建对象

类似于CAS(Compare And Set “比较并交换”)现在有个数据为0一堆线程对这条数据进行递增,如何保证数据的一致性。

一种方法是递增的上锁。
第二种方法先把0读到自己的线程内存里面来读完之后,改为1。然后再写回去把0写成1
(写法是 先判断你是不是依然为0,如果依然为0就说明再改线程进行中没有其他线程改动过,就可以直接改为1。如果当判断这个数已经被线程改为8了,这时候就把8拿过来判断是否为8,如果是就改为9,这就是自旋锁)

这种自旋锁还存在ABA的问题


你把0改为1的写法中判断依然是0,这时候你以为没有被其他线程改动过这个0,其实还有一种情况就是再你把0改为1的过程中另外一个线程已经把0改为3,然后再有第三个线程把这个3改回为0。再第一个线程看来就是没有其他线程改动过。这种使用版本号进行解决,没修改一次加一个版本号。

DCL单例需不需要加volatile?

  1. 先检查INSTANCE是否为空,如果为空说明还没有任何一个线程给他初始化
  2. 如果为空,上锁,我来对他进行初始化
  3. 再一次进行检查,(因为在加锁的期间有可能被别的线程进行占用)
  4. 进行第二次检查,确保刚才没有线程进行初始化
  5. 我们对其进行初始化

Mgr06需不需要加volatile?

结论是要加的。
不加上volatile会发生一下情况:

  1. 检查(先判断t是否为空),当第一个线程加上锁
  2. 上锁完,new这个对象
  3. 但是new这个对象刚new了一半的时候,m的值为0赋默认值
  4. 正好再这个时间发生了指令重排
  5. 这时候就先建立与class的关联关系(astore_1是把这个引用值赋值到小t)后进行调用构造方法(invokespecial 是调用构造方法 把内存中的m原来为0现在变为8)先建立关联关系的时候t不为空,它指向new出来一半的初始化对象(半初始化状态)
  6. 当初始化到一半的时候另外一个线程来了
  7. 但是现在的INSTANCE已经是个半初始化的状态,不为空
  8. 那第二个现在就直接用初始值了,不用默认值
  9. 就导致数据不一致问题

解决问题的关键加volatile 指令重排?


因为volatile禁止指令重排序

总结:
单例模式双重检测为什么要加volatile?
因为指令可能重排,在创建对象的时候应该先调用构造方法在进行引用赋值astore_1,但是由于指令有可能会重排,这两个先后顺序会不一样。

半初始化

this逃逸

对象的半初始化状态    指令的重排序 两者结合到一起的话就会出现this对象的逃逸

通过this访问对象的属性的值时候得到的是对象半初始化状态的值,这些值在java里面是变量类型的默认值。而不是真实设置的属性值.这种情况称为this逃逸

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值