Java CAS机制详解
作者:氵奄不死的鱼
一、什么是CAS
什么是CAS机制
CAS机制是一种数据更新的方式。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。
悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态**,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable华的时间可能比你的更新操作的时间还要长。**
乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。
CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:
- 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
- 工作内存中共享变量的副本值,也叫预期值:A
- 需要将共享变量更新到的最新值:B
CAS,compare and swap的缩写,中文翻译成比较并交换。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
为何CAS如此优秀
硬件加持,现代大多数处理器都从硬件层面通过一些列指令实现CompareAndSwap(比较并交换)同步原语,进而使操作系统和JVM可以直接使用这些指令实现锁和并发的数据结构。我们可以简单认为,CAS是将比较和交换合成是一个原子操作。
JVM对CAS的支持, 由于Java程序运行在JVM上,所以应对不同的硬件体系架构的处理则需要JVM来实现。在不支持CAS操作的硬件上,jvm将使用自旋锁来实现。
CAS为什么要和volitile配合使用
cas保证原子性。volitile保证可见性和有序性,二者加起来,保证线程安全!
二、Java中的Atomic原子操作包
JUC 并发包中原子类 , 都存放在 java.util.concurrent.atomic 类路径下:
根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
基本原子类
数组原子类
原子引用类型
字段更新原子类
1. 基本原子类
基本原子类的功能,是通过原子方式更新 Java 基础类型变量的值。基本原子类主要包括了以下三个:
- AtomicInteger:整型原子类。
- AtomicLong:长整型原子类。
- AtomicBoolean :布尔型原子类。
2. 数组原子类
数组原子类的功能,是通过原子方式更数组里的某个元素的值。数组原子类主要包括了以下三个:
- AtomicIntegerArray:整型数组原子类。
- AtomicLongArray:长整型数组原子类。
- AtomicReferenceArray :引用类型数组原子类。
3. 引用原子类(可以奖多个数据聚合成一个对象坐cas操作,但是不要直接修改原对象,而是每次复制处新对象,在新对象生更改后,通过cas设置回去)。
引用原子类主要包括了以下三个:
- AtomicReference:引用类型原子类
- AtomicMarkableReference :带有更新标记位的原子引用类型。
- AtomicStampedReference :带有更新版本号的原子引用类型。
AtomicStampedReference通过引入“版本”的概念,来解决ABA的问题。
4. 字段更新原子类
字段更新原子类主要包括了以下三个:
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
- AtomicLongFieldUpdater:原子更新长整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
三、类AtomicInteger
1、常用的方法:
方法 介绍
public final int get() 获取当前的值
public final int getAndSet(int newValue) 获取当前的值,然后设置新的值
public final int getAndIncrement() 获取当前的值,然后自增
public final int getAndDecrement() 获取当前的值,然后自减
public final int getAndAdd(int delta) 获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) 通过 CAS 方式设置整数值
AtomicInteger 案例:
private static void out(int oldValue,int newValue){ System.out.println("旧值:"+oldValue+",新值:"+newValue); } public static void main(String[] args) { int value = 0; AtomicInteger atomicInteger= new AtomicInteger(0); //取值,然后设置一个新值 value = atomicInteger.getAndSet(3); //旧值:0,新值:3 out(value,atomicInteger.get()); //取值,然后自增 value = atomicInteger.getAndIncrement(); //旧值:3,新值:4 out(value,atomicInteger.get()); //取值,然后增加 5 value = atomicInteger.getAndAdd(5); //旧值:4,新值:9 out(value,atomicInteger.get()); //CAS 交换 boolean flag = atomicInteger.compareAndSet(9, 100); //旧值:4,新值:100 out(value,atomicInteger.get()); }
2、AtomicInteger 源码解析:
public class AtomicInteger extends Number implements java.io.Serializable { // 设置使用Unsafe.compareAndSwapInt进行更新 private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } ...省略 private volatile int value; //自动设置为给定值并返回旧值。 public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } //以原子方式将当前值加1并返回旧值。 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } //以原子方式将当前值减1并返回旧值。 public final int getAndDecrement() { return unsafe.getAndAddInt(this, valueOffset, -1); } //原子地将给定值添加到当前值并返回旧值。 public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } ...省略 }
通过源码我们发现AtomicInteger的增减操作都调用了Unsafe 实例的方法,下面我们对Unsafe类做介绍:
四、Unsafe类
Unsafe 是位于 sun.misc 包下的一个类,Unsafe 提供了CAS 方法,直接通过native 方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。
Unsafe类,翻译为中文:危险的,Unsafe全限定名是 sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般应用开发者不会用到这个类。
1、Unsafe 提供的 CAS 方法
主要如下: 定义在 Unsafe 类中的三个 “比较并交换”原子方法
/* @param o 包含要修改的字段的对象 @param offset 字段在对象内的偏移量 @param expected 期望值(旧的值) @param update 更新值(新的值) @return true 更新成功 | false 更新失败 */ public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); public final native boolean compareAndSwapInt( Object o, long offset, int expected,int update); public final native boolean compareAndSwapLong( Object o, long offset, long expected, long update);
Unsafe 提供的 CAS 方法包含四个入参: 包含要修改的字段对象、字段内存位置、预期原值及
新值。在执行 Unsafe 的 CAS 方法的时候,这些方法首先将内存位置的值与预期值(旧的值)比
较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值,并返回 true ;如果不相匹配,
处理器不做任何操作,并返回 false 。
2、获取属性偏移量
Unsafe 提供的获取字段(属性)偏移量的相关操作,主要如下:
/** * @param o 需要操作属性的反射 * @return 属性的偏移量 */ public native long staticFieldOffset(Field field); public native long objectFieldOffset(Field field);
staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量,在 CAS 操作静态属性时,会用到这个偏移量。
objectFieldOffset 方法用于获取非静态 Field (非静态属性)在 Object 实例中的偏移量,在 CAS 操作对象的非静态属性时,会用到这个偏移量。
3、根据属性的偏移量获取属性的最新值:
/** * @param o 字段所属于的对象实例 * @param fieldOffset 字段的偏移量 * @return 字段的最新值 */ public native int getIntVolatile(Object o, long fieldOffset);
五、CAS的缺点
ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
JDK 提供了两个类 AtomicStampedReference、AtomicMarkableReference 来解决 ABA 问题。
只能保证一个共享变量的原子操作。一个比较简单的规避方法为:把多个共享变量合并成一个共享变量来操作。 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,可以把多个变量放在一个 AtomicReference 实例后再进行 CAS 操作。比如有两个共享变量 i=1、j=2,可以将二者合并成一个对象,然后用 CAS 来操作该合并对象的 AtomicReference 引用。
循环时间长开销大。高并发下N多线程同时去操作一个变量,会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。
解决 CAS 恶性空自旋的较为常见的方案为:
分散操作热点,使用 LongAdder 替代基础原子类 AtomicLong。
使用队列削峰,将发生 CAS 争用的线程加入一个队列中排队,降低 CAS 争用的激烈程度。JUC 中非常重要的基础类 AQS(抽象队列同步器)就是这么做的。
六、以空间换时间LongAdder
1、LongAdder 的原理
LongAdder 的基本思路就是分散热点, 如果有竞争的话,内部维护了多个Cell变量,每个Cell里面有一个初始值为0的long型变量, 不同线程会命中到数组的不同Cell (槽 )中,各个线程只对自己Cell(槽) 中的那个值进行 CAS 操作。这样热点就被分散了,冲突的概率就小很多。 在没有竞争的情况下,要累加的数通过 CAS 累加到 base 上。 如果要获得完整的 LongAdder 存储的值,只要将各个槽中的变量值累加,后的值即可。
七、使用AtomicStampedReference解决ABA问题
JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。
AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 整型 印戳(或标记),使用这个印戳可以来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用, 并且当前印戳( Stamp )标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳 ( Stamp )标志的值更新为给定的更新值。 1、AtomicStampReference 的构造器:
/** * @param initialRef初始引用 * @param initialStamp初始戳记 *\ AtomicStampedReference(V initialRef, int initialStamp)
2、AtomicStampReference 的常用的几个方法如下:
方法 介绍
public V getRerference() 引用的当前值 public int getStamp() 返回当前的"戳记"
public boolean weakCompareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) expectedReference 引用的旧值 newReference 引用的新值 expectedStamp 旧的戳记 newStamp 新的戳记
public static void main(String[] args) { boolean success = false; AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(1, 0); int stamp = atomicStampedReference.getStamp(); success = atomicStampedReference.compareAndSet(1, 0, stamp, stamp + 1); System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp()); //修改印戳,更新失败 stamp = 0; success = atomicStampedReference.compareAndSet(0, 1, stamp, stamp + 1); System.out.println("success:" + success + ";reference:" + "" + atomicStampedReference.getReference() + ";stamp:" + atomicStampedReference.getStamp()); }
八、AtomicIntegerFieldUpdater进行字段更新
如果一个类是自己编写的,则可以在编写的时候把成员变量定义为Atomic类型。但如果是一个已经有的类,在不能更改其源代码的情况下,要想实现对其成员变量的原子操作,就需要AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。
public static void main(String[] args) { Student student = new Student(); //创建AtomicIntegerFieldUpdater对象 AtomicIntegerFieldUpdater<Student> studentAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age"); //打印age并将age+1 System.out.println(studentAtomicIntegerFieldUpdater.getAndIncrement(student)); System.out.println(student.age); } //测试类 public class Student { //因为是用反射实现的这里必须要使用public修饰 public volatile int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
要修改的age必须是int不能是包装类Integer,必须被volatile修饰
if (field.getType() != int.class) throw new IllegalArgumentException("Must be integer type"); if ( ! Modifier.isVolatile(modifiers)) throw new IllegalArgumentException( "Must be volation type");
AtomicIntegerFieldUpdater代码实现
//AtomicIntegerFieldUpdater的无参构造被protected修饰,使用newUpdater创建对象 protected AtomicIntegerFieldUpdater() { } public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) { return new AtomicIntegerFieldUpdaterImpl<U> (tclass, fieldName, Reflection.getCallerClass()); }
public final int getAndIncrement(T obj) { //age+1 return getAndAdd(obj, 1); } public final int getAndAdd(T obj, int delta) { //检查obj和声明AtomicIntegerFieldUpdater时的class一不一样 accessCheck(obj); //Unsafe类获得age的值并且使用compareAndSwapInt将age+1 return U.getAndAddInt(obj, offset, delta); }
AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater和AtomicIntegerFieldUpdater类似
到此这篇关于Java CAS机制详解的文章就介绍到这了,更多相关Java CAS内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!