Java 中共享变量的内存可见性问题 + volatile + CAS

一、Java 中共享变量的内存可见性问题

在多线程下处理共享变量时Java 的内存模型,如下图所示。

在这里插入图片描述

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。Java 内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?请看图2-5 。
在这里插入图片描述

图中所示是一个双核CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。那么Java 内存模型里面的工作内存,就对应这里的Ll或者L2缓存或者CPU的寄存器。
当一个线程操作共享变量时, 它首先从主内存复制共享变量到自己的工作内存, 然后对工作内存里的变量进行处理, 处理完后将变量值更新到主内存。
那么假如线程A 和线程B 同时处理一个共享变量, 会出现什么情况?我们使用图2 - 5所示CPU 架构, 假设线程A 和线程B 使用不同CPU 执行,并且当前两级Cache 都为空,那么这时候由于C ache 的存在,将会导致内存不可见问题, 具体看下面的分析。
• 线程A 首先获取共享变量X 的值,由于两级Cache 都没有命中,所以加载主内存中X 的值,假如为0。然后把X=O 的值缓存到两级缓存, 线程A修改X 的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A 所在的CPU 的两级Cache 内和主内存里面的X 的值都是1 。
• 线程B 获取X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X= 1 ; 到这里一切都是正常的, 因为这时候主内存中也是X= 1 。然后线程B 修改X 的值为2, 并将其存放到线程2 所在的一级Cache 和共享二级Cache 中,最后更新主内存中X 的值为2 到这里一切都是好的。
• 线程A 这次又需要修改X 的值, 获取时一级缓存命中,并且X= 1,到这里问题就出现了,明明线程B 已经把X 的值修改为了2 ,为何线程A 获取的还是1呢? 这就是共享变量的内存不可见问题, 也就是线程B 写入的值对线程A 不可见。
那么如何解决共享变量内存不可见问题? 使用Java 中的volatile关键字就可以解决这个问题, 下面会有讲解。

二、volatile

在并发编程中synchronized和volatile都扮演着重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
并非在所有情况下使用它们都是等价的, volatile虽然提供了可见性保证,但并不保证操作的原子性。那么一般在什么时候才使用volatile 关键字呢?
• 写入变量值不依赖、变量的当前值时。因为如果依赖当前值,将是获取一计算一写入三步操作,这三步操作不是原子性的,而volatile 不保证原子性。
• 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatile 的。
示例:

public class ThreadSafeinteger {
private volatile int value ;
    public int get() (
        return value;
    }
        
     public void set (int value) {
        this .value = value ;
    }
        
}

2.1 为什么volatile不保证原子性

原子性:不可分割的,也即某个线程正在做某个具体业务时,中间不可以被加塞,需要整体完整。要么同时成功,要么同时失败。
因为根据JMM Java内存模型我们可以知道,会先从主内存中获取,再计算,最后写入。看字节码文件也可以看出来,虽然加了volatile关键,还是将是获取一计算一写入三步操作,这三步操作不是原子性的,所以volatile 不保证原子性。
解决原子性的问题:使用原子操作类:原子操作类都使用CAS 非阻塞算法,性能更好。Atomiclnteger。

三、CAS

CAS(Compare and Swap 比较并交换)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS操作中包含三个操作数——需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,否则处理器不做任何操作。
它都会在CAS指令之前返回该位置的值,CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可”。这其实和乐观锁的冲突检查+数据更新的原理是一样的。

3.1 Unsafe

unsafe是CAS的核心类,因为Java方法无法直接访问底层系统,需要通过本地native方法来访问,unsafe相当于是一个后门,基于该类可以直接操作特定内存数据。

3.2 CAS的缺点
1 如果CAS失败,会一直进行重试,如果CAS长时间不成功,可能会给cpu带来很大的开销。
2 只能保证一个共享变量的原子操作。
3 ABA问题-原子操作类ABA的问题

3.2.1 ABA问题

CAS算法实现的前提是取出来内存中某个时刻的数据,进行比较,然后写入替换,那么在这个时间差可能会出现数据的变化。
比如说两个线程1、线程2、两个线程都把同一个数据从内存中取出来为A,线程2进行了一些操作把值改为了B,然后又将数据改回了A,这个时候线程1进行数据修改的时候,发现内存中仍然是A,然后就操作成功了。
虽然说是修改成功了,但是不代表这个过程是没有问题的。

3.2.2 ABA问题的解决

借助原子引用类AtomicStampedReference,比较值+版本号。在进行设置新的值的同时也会比较修改版本号。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值