Java 多线程 : 漫谈 CAS

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 https://juejin.cn/post/6941642435189538824
Github : 👉 https://github.com/black-ant
CASE 备份 : 👉 https://gitee.com/antblack/case

一 . CAS 简介

什么是 CAS ?

CAS操作 —— Compare & Set ,或是 Compare & Swap


CAS 的操作步骤是什么 ? -> 先比较 , 再设置

jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。

  1. CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。
  2. 当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做

CAS 的效率 ?

  • CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快
  • CAS避免了请求操作系统来裁定锁的问题

CAS 的消耗?

一个8核CPU计算机系统,每个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核可以互相通信


当存在 cache 和 数据不在一个域中时 ?

“最好情况”是指对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU,所以对应的缓存线已经在 CPU 的高速缓存中了


算法假想

do{   
       备份旧数据;  
       基于旧数据构造新数据;  
}while(!CAS( 内存地址,备份的旧数据,新数据 ))  
    

二 . CAS 的缺陷

问题一 : ABA 问题

  1. 一个线程 one 从内存位置 V 中取出 A
  2. 另一个线程 two 也从内存中取出 A ,并且 two 进行了一些操作变成了 B
  3. two 又将 V 位置的数据变成 A ,这时候线程 one 进行 CAS 操作发现内存中仍然是 A
  4. one 操作成功。
  • 尽管线程 one 的 CAS 操作成功,但可能存在潜藏的问题。

  • 从 Java5 开始 JDK 的 atomic包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。

    • AtomicStampedReference 通过包装 [E,Integer] 的元组,来对对象标记版本戳 stamp

问题二 : 循环时间长开销大

对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

问题三 : 只能保证一个共享变量的原子操作

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。

三 . CAS 深入分析

在 CAS 中有三个参数:内存值 V、旧的预期值 A、要更新的值 B ,当且仅当内存值 V 的值等于旧的预期值 A 时,才会将内存值V的值修改为 B ,否则什么都不干

主要类 : Unsafe

Unsafe 是 CAS 的核心类 , 他提供了硬件级别得原子操作 (其他情况下Java 需要通过本地 Native 方法访问底层操作系统)

  • unsafe.objectFieldOffset
  • getAndAddInt -> compareAndSwapInt(Object var1, long var2, int var4, int var5)
    • 对以下四个值进行了比较判断 : 对象、对象的地址、预期值、修改值

CPU 的原子操作: CPU 提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁

  • 总线加锁:

    总线加锁就是就是使用处理器提供的一个 LOCK# 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。所以就有了缓存加锁。

  • 缓存加锁:

    其实针对于上面那种情况,我们只需要保证在同一时刻,对某个内存地址的操作是原子性的即可。缓存加锁,就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不再输 出LOCK# 信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当 CPU1 修改缓存行中的 i 时使用缓存锁定,那么 CPU2 就不能同时缓存了 i 的缓存行。

CAS 主要实现得方式 :

  • AtomicInteger
    • addAndGet()

四 . CAS CPU 的查询操作

@ https://blog.csdn.net/youanyyou/article/details/111879515

     
// CPU 结构简述 : 
- 每个CPU有cache(CPU内部的高速缓存,寄存器)
- 管芯内还带有一个互联模块,使管芯内的两个核可以互相通信
- 系统互联模块可以让四个管芯相互通信,并且将管芯与主存连接起来
        
// 数据流动
- 数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小通常为 32 到 256 字节之间
- 当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存
- CPU 将寄存器中的一个值存储到内存时,不仅必须将包含了该值的缓存线读到 CPU 高速缓存,
    还必须确保没有其他 CPU 拥有该缓存线的拷贝 
    
// 转变流程
• CPU0 检查本地高速缓存,没有找到缓存线。
• 请求被转发到 CPU0 和 CPU1 的互联模块,检查 CPU1 的本地高速缓存,没有找到缓存线。
• 请求被转发到系统互联模块,检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有。
• 请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线。
• CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线。
• CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块。
• 系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块。
• CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存。
• CPU0 现在可以对高速缓存中的变量执行 CAS 操作了 
    
//效率问题
最好情况 : 某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU 
        

五 . CAS 初级原理

// Java 里面 CAS 操作主要通过 Native 方法完成 , 主要的操作对象有 : Unsafe 

C- Unsafe
    - 提供了硬件级别的原子操作
	- 对于Unsafe类的使用都是受限制的,只有授信的代码才能获得该类的实例
	M- public native long allocateMemory(long paramLong) : 
	M- public native long reallocateMemory(long paramLong1, long paramLong2) : 扩充内存
	M- public native void freeMemory(long paramLong) : 释放内存
    
    
// 例如 AQS 里面 : 
> AbstractQueuedSynchronizer
	return unsafe.compareAndSwapObject(this, headOffset, null, update);


	

六 . CAS 深入

@ https://blog.csdn.net/qq_37113604/article/details/81582784

// .c 文件 sun.misc.Unsafe
public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);

// C 源码
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
 
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}


// 总结为 : 
程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。
如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。
反之,如果程序是在单处理器上运行,就省略lock前缀
    ?- (单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

附录 : Git 源码

源码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值