编辑
哈喽,大家好~我是你们的老朋友保护小周ღ,本期为大家带来的是 CAS (compare and swap) 比较并交换,CAS 是物理层次支持程序的原子操作, CAS 是一种完全不同于 synchronized 锁保证多线程安全问题的机制,可以用来进行无锁编程,讲述了 CAS 的概率,使用场景,以及优缺点,确定不来看看嘛~更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘
编辑
一、什么是 CAS
CAS (compare and swap) 比较并交换,CAS 是物理层次支持程序的原子操作。说起原子性,这就设计到线程安全问题,站在代码的层面为了解决多线程并发处理同一共享资源造成的线程安全问题,我们常常会使用 synchronized 修饰代码块,变量等,将程序背后的指令封装成原子性(事务的最小单位,不可再分),当一个线程执行 synchronized 修饰的代码块时获取指定对象的对象锁(一个对象只有一把锁),其他并发处理同一代码块的线程因无法获取对象锁就会进入阻塞等待对象锁释放,然后继续竞争对象锁,此时 synchronized 修饰的代码块就具有原子性,具有互斥性,不可抢占性。
Object locker = new Object; //使用第三方对象锁 synchronized(locker) { //尝试获取 locker 的对象锁,获取成功继续执行,失败线程阻塞等待 //具有原子性的代码块 } // 同步代码块执行完毕,自动释放对象锁,其他线程就可以获取对象锁
CAS 是CPU 物理层次支持的原子操作(一条指令).
CAS 操作包含三个操作数:
读取内存数据(V),预期原值(A)和新值(B)。
如果内存位置的值与预期原值相等,就将新值(B)更新到内存中,替换掉原内存数据,
如果内存位置的值与预期原值不相等,处理器不会做任何操作,
CAS 对数据操作后会返回一个 boolean 值判断是否成功。
以上指令集合,可以视为CPU 物理层次支持的一条指令,同样可以解决多线程并发处理同一共享资源造成的线程安全问题。
CAS 使用Java伪代码理解含义:
// address 原内存值 // expectValue 旧的预期值 // swapValue 需要修改的新值 // & 代表取地址,这里主要是理解这层含义,java 语法不支持 boolean CAS(address, expectValue, swapValue) { if (&address == expectedValue) { &address = swapValue; return true; } return false; }
举个例子(多线程同时对同一共享数据进行处理引起的线程安全问题):
编辑
使用 CAS 后:
编辑
CAS 是一种完全不同于 synchronized 锁保证多线程安全问题的机制。
Java 伪代码方便理解上述例题操作大概是怎么运行的。
private int count = 0; public int getAndIncrement() { int oldValue = count; while ( CAS(value, oldValue, oldValue+1) != true) { oldValue = value; } return oldValue; }
本来以上这样的操作站在代码角度不是原子性有多条指令, 但是 CAS 在硬件层面上支持一条指令完成这个操作, 所以也就变成原子的了。
根据以上事例、伪代码,我们会发现,当多线程企图对 count 进行自增时,同一时间内只有一个线程能成功修改数据,其他线程会因为一直不符合以下三条规定,而循环的访问内存,直到条件成立
读取内存数据(V),预期原值(A)和新值(B)。
1. 比较 A 与 V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V。(交换),否则啥也不干
3. 返回操作是否成功。
二、CAS 的缺点
1.多线程并发执行,同一单位时间内只有一个线程能处理数据成功,此时其他共同想处理 count 的线程循环的读取内存数据,进行CAS 判断, 这样就造成了 CPU 资源的消耗。
从某种意义上来说也提升了多线程处理数据的速度,如果 synchronized 锁在锁冲突严重的情况下,竞争锁失败的线程会进入阻塞等待(暂时不参与CPU 的调度执行),这样一来CPU 的工作效率会提升,但是释放锁的后,需要将等待的线程唤醒,这也是一笔开销,如果 synchronized 在锁冲突不严重的情况下,会采用 “自旋锁” 的方式来实现,自旋就是一直循环的尝试获取对象锁,所以当对象锁被释放,自旋线程能够保证第一时间拿到锁,这一点是不是跟 CAS 操作很像,没错,synchronized 的自旋属性就是根据 CAS 机制实现的,
synchronized 会根据当前锁竞争的程度,自适应的转化锁属性
2. CAS机制所保证的只是一个变量(一段数据的)的原子性操作,而不能保证整个代码块的原子性。比如需要保证多个变量共同进行原子性的更新,或者一段程序的逻辑,就不得不使用synchronized 修饰了。
3. BAB 问题
这个问题也是 CAS 机制中不符合常理的问题
CAS 机制的关键在于比较内存和寄存器的值,看看是否相同,就是通过这个比较,来判断内存是不是被修改过,如果对比的时候是相同的,但不代表内存中的值在内读取之前没有变动过。
例如: 有线程 A,B,C, 有整型变量 value = 10;
A 线程先将 value 值从内存中读取到 寄存器中,同一时刻,线程 B 将 value 值修改为 100,线程 C 又将 value 值 100 修改成了 10, 此时 线程 A 需要再从内存中读取一次 value 用来校验 value 是否被修改过,这个时候读取的是10 ,与寄存器中的值相等,value数据可以更新,站在线程A 的角度上看,value 的数据没有被修改,但是站在上帝视角来看,value 的值已经被线程 B、C 修改过了,万一对比的时候是相同的,但是不代表 value 值没有变动过,从 a -> b - > a,这个过程中就有一定的概率会出现问题。
要解决ABA的问题,Java也提供了AtomicStampedReference类供我们用,就是加了个版本,比对的就是内存值 + ”版本号“是否一致,版本号约定数据一次只能单方向的变化(数值只能增加或者是减小),如果需要数据既能增加也能减小,这个时候可以引入另外一个版本号变量,约定版本号只能增加或者是减少,此时每次 CAS 对比两组数据的时候,不是对比数据本身,而是对比版本号,以版本号为基准,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。
三、CAS 的应用
3.1 CAS 实现原子类
java使用Unsafe类来支持CAS操作,Unsafe 类在sun.misc 包下,不属于 Java 标准库,但是有很多Java 的标准库中使用 Unsafe 类开发,Unsafe类使Java代码拥有了像C语言的指针一样操作内存空间的能力(Unsafe里面的方法都是 native方法,可以访问本地C++实现库,JVM 底层逻辑就有 大量基于 C/C++ 实现的部分,也可以访问操作系统的底层,操作系统的底层也是 C/ C++ 是实现的),同时也带来了指针的问题,Unsafe类也提供了许多硬件级别的原子操作。
Java 标准库中 AtomicInteger 类就是一个提供原子操作的 Integer 的类,他的底层方式就是使用了Unsafe 类中的方法作为支持,也就是说同样使用了 CAS 机制。
public class Demo1 { public static void main(String[] args) { // 将期望值设置为 5 AtomicInteger atomicInteger = new AtomicInteger(5); // 确定是否还是等于 5 ,如果匹配成功就将新值更新到内存,覆盖掉atomicInteger的值; 返回 boolean值 System.out.println(atomicInteger.compareAndSet(5, 2020)); System.out.println("data:" + atomicInteger.get()); // 确定atomicInteger值是否还是等于 5,如果是更新为 2023 System.out.println(atomicInteger.compareAndSet(5, 2023)); System.out.println("data:" + atomicInteger.get()); } }
编辑
AtomicInteger 类内部方法
编辑
编辑
AtomicInteger 类是一个可以在多线程环境中保证一致性的类,使用起来简单,可以大大提高多线程开发程序的安全性和可能性,是无锁编程的机制。
到这里,CAS (compare and swap) 机制的概念,优缺点,使用场景,博主已经分享完了,希望对大家有所帮助,如有不妥之处欢迎批评指正。
编辑
本期收录于博主的专栏——JavaEE,适用于编程初学者,感兴趣的朋友们可以订阅,查看其它“JavaEE基础知识”。
感谢每一个观看本篇文章的朋友,更多精彩敬请期待:保护小周ღ *★,°*:.☆( ̄▽ ̄)/$:*.°★* ‘