1. 什么是CAS?
CAS 的英文全称为 Compare and Swap,翻译成中文为“比较并交换”。
1.1 CAS详解
CAS 是一种无锁算法,该算法关键依赖两个值——期望值(旧值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等,如果相等则给内存地址赋新值,否则不做任何操作。使用 CAS 进行“无锁编程”(Lock Free)的步骤大致如下:
1.获得字段的期望值(oldValue)。
2.计算出需要替换的新值(newValue)。
3.通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功,这种重复俗称 CAS 自旋。
使用CAS进行“无锁编程”的伪代码,如下:
do {
获得字段的期望值(oldValue);
计算出需要替换的新值(newValue);
} while (!CAS(内存地址,oldValue,newValue))
举一个简单的例子对上述代码进行说明:
假如某个内存地址(某对象的属性)的值为 100,现在有两个线程(线程 A、线程 B)使用CAS 无锁编程对该内存地址进行更新,线程 A 欲将其值更新为 200,线程 B 欲将其值更新为 300,如图所示:
由于线程是并发执行,谁都有可能先执行。但是 CAS 是原子操作,对同一个内存地址的 CAS操作在同一时刻只能执行一个。所以在这个例子中,要么线程 A 先执行,要么线程 B 先执行。假设线程 A 的 CAS(100,200)执行在前,由于内存地址的旧值 100 与该 CAS 的期望值 100 相等,所以线程 A 会操作成功,内存地址的值被更新为 200。
线程 A 执行成功 CAS(100,200)之后,内存地址的值如图所示:
接下来执行线程 B 的 CAS(100,300)操作,此时内存地址的值为 200,不等于 CAS 的期望值 100,线程 B 操作失败。线程 B 只能自旋,开始新的循环,这一轮循环首先获取到内存地址的值 200,然后进行 CAS(200,300)操作,这一次内存地址的值与 CAS 的预期值(oldValue)相等,线程 B 操作成功。
当 CAS 进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改,可 以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS 性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多,CAS 性能会大大降低。所以,提升 CAS 无锁编程的效率,关键在于减少冲突的机会。
1.2 操作系统中的CAS
操作系统层面的CAS一条CPU的原子指令(cmpxchg指令),正是由于该指令具备了原子性,所以CAS操作不会造成数据不一致的问题,Unsafe提供的CAS方法,直接通过native方式(封装C++代码)调用了底层CPU指令cmpxchg。
2. Unsafe类
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全的底层操作。如直接访问系统内存资源、自主管理内存资源等,Unsafe 大量的方法都是 native 方法,基于 C++语言实现,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
为什么此类取名为 Unsafe 呢?
我们都知道Unsafe有不安全的意思,由于使用 Unsafe 类可以像 C 语言一样使用指针操作内存空间,这无疑增加了指针相关问题、内存泄露问题的出现概率。总之,在程序中过度使用 Unsafe 类,会使得程序出错的概率变大,使得安全的语言 Java 变得不再“安全”,因此对 Unsafe 的使用一定要慎重。
完成 Java 应用层的 CAS 操作,主要涉及到的 Unsafe 方法调用,具体如下:
(1)获取 Unsafe 实例。
(2)调用 Unsafe 提供的 CAS 方法,这些方法主要封装了底层 CPU 的 CAS 原子操作。
(3)调用 Unsafe 提供的字段偏移量方法,这些方法用于获取对象中的字段(属性)偏移量,此偏移量值需要作为参数提供给 CAS 操作。
2.1 内存管理:Unsafe类提供的直接操纵内存相关的方法
//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);
//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte getByte(long address);
public native void putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
.......... 省略代码
//操作系统的内存页大小
public native int pageSize();
2.2 对象实例创建:Unsafe类提供创建对象实例新的途径
//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
2.3 类、实例对象以及变量操作:Unsafe类提供类、实例对象以及变量操纵方法
//获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
//通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
//设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);
//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同
//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile
//及getIntVolatile相同,引用类型putObjectVolatile也一样。
..........省略代码
//与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o,long offset,int x);
Unsafe 类是一个“final”修饰的不允许继承的最终类,Unsafe类是没有对外提供构造函数的,虽然Unsafe类对外提供getUnsafe()方法,但该方法只提供给Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在下面的Demo中使用反射技术获取了Unsafe实例对象并进行相关操作。
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
public class UnSafeDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
// 通过反射得到theUnsafe对应的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该Field为可访问
field.setAccessible(true);
// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
//通过allocateInstance直接创建对象
Demo demo = (Demo) unsafe.allocateInstance(Demo.class);
Class demoClass = demo.getClass();
Field str = demoClass.getDeclaredField("str");
Field i = demoClass.getDeclaredField("i");
Field staticStr = demoClass.getDeclaredField("staticStr");
//获取实例变量str和i在对象内存中的偏移量并设置值
unsafe.putInt(demo,unsafe.objectFieldOffset(i),1);
unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!");
// 返回 User.class
Object staticField = unsafe.staticFieldBase(staticStr);
System.out.println("staticField:" + staticStr);
//获取静态变量staticStr的偏移量staticOffset
long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr"));
//获取静态变量的值
System.out.println("设置前的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//设置值
unsafe.putObject(staticField,staticOffset,"Hello Java!");
//再次获取静态变量的值
System.out.println("设置后的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//调用toString方法
System.out.println("输出结果:"+demo.toString());
long data = 1000;
byte size = 1; //单位字节
//调用allocateMemory分配内存,并获取内存地址memoryAddress
long memoryAddress = unsafe.allocateMemory(size);
//直接往内存写入数据
unsafe.putAddress(memoryAddress, data);
//获取指定内存地址的数据
long addrData = unsafe.getAddress(memoryAddress);
System.out.println("addrData:"+addrData);
/**
* 输出结果:
sun.misc.Unsafe@0f18aef2
staticField:class com.demo.Demo
设置前的Static字段值:Demo_Static
设置后的Static字段值:Hello Java!
输出USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java!'}
addrData:1000
*/
}
}
class Demo{
public Demo(){
System.out.println("我是Demo类的构造函数,我被人调用创建对象实例啦....");
}
private String str;
private int i;
private static String staticStr = "Demo_Static";
@Override
public String toString() {
return "Demo{" +
"str = '" + str + '\'' +
", i = '" + i +'\'' +
", staticStr = " + staticStr +'\'' +
'}';
}
}
2.4 数组操作:Unsafe类提供直接获取数组元素内存位置的途径
//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);
2.5 CAS相关操作:Unsafe类提供Java中的CAS操作支持
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
2.6 JDK8之后新增的基于原有CAS方法的方法
//1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取内存中最新值
v = getIntVolatile(o, offset);
//通过CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增,方法作用同上,只不过这里操作的long类型数据
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
//1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
// 1.8新增,同上,操作的是long类型
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
//1.8新增,同上,操作的是引用类型数据
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
3. CAS实现原子操作的三大问题
3.1 ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化 则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它 的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从 Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个 类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是 否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
public boolean compareAndSet(
V expectedReference,// 预期引用
V newReference, // 更新后的引用
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
)
如何解决这种ABA问题?
解决方法: AtomicReference原子引用。 接下来会详细介绍原子类以及如何使用原子类解决ABA问题。
3.2 循环时间长开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:
第 一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间 取决于具体实现的版本,在一些处理器上延迟时间是零;
第二,它可以避免在退出循环的时候 因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而 提高CPU的执行效率
3.3 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循 环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子 性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来 操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始, JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对 象里来进行CAS操作。
4. J.U.C包下的原子操作包Atomic
Atomic 操作翻译成中文,是指一个不可中断的操 作,即使在多个线程一起执行 Atomic 类型操作的时候,一个操作一旦开始,就不会被其他线程中断。所谓Atomic 类,指的是具有原子操作特征的类。
4.1 JUC 并发包中原子类的位置
JUC 并 发 包 中 原 子 类 , 都 存 放 在java.util.concurrent.atomic 类路径下,如图:
根据操作的目标数据类型,可以将 JUC 包中的原子类分为 4 类:
(1)基本原子类,(2)数组原子类, (3)原子引用类型,(4)字段更新原子类。
4.1.1 基本原子类
基本原子类的功能,是通过原子方式更新 Java 基 础类型变量的值。基本原子类主要包括了以下三个:
- AtomicInteger:整型原子类。
- AtomicLong:长整型原子类。
- AtomicBoolean :布尔型原子类。
4.1.2 数组原子类
数组原子类的功能,是通过原子方式更数组里的某个元素的值。数组原子类主要包括了以下三个:
- AtomicIntegerArray:整型数组原子类。
- AtomicLongArray:长整型数组原子类。
- AtomicReferenceArray :引用类型数组原子类。
4.1.3 引用原子类
- AtomicReference:引用类型原子类。
- AtomicMarkableReference :带有更新标记位的原子引用类型。
- AtomicStampedReference :带有更新版本号的原子引用类型。
AtomicMarkableReference 类将 boolean 标记与引用关联起来,可以解决使用AtomicBoolean进行原子更新时可能出现的 ABA 问题。
AtomicStampedReference 类将整数值与引用关联起来,可以解决使用 AtomicInteger 进行原子更新时可能出现的 ABA 问题。
4.1.4 字段更新原子类
字段更新原子类主要包括了以下三个:
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器。
- AtomicLongFieldUpdater:原子更新长整型字段的更新器。
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
4.2 基础原子类 AtomicInteger
基础原子类 AtomicInteger 常用的方法主要如下:
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 的使用示例,具体代码如下:
public class AtomicTest {
public static void main(String[] args) {
int tempValue = 0;
// 定义一个整数原子类实例,赋值到变量 i
AtomicInteger i = new AtomicInteger(0);
// 取值,然后设置一个新值
tempValue = i.getAndSet(3);
// 输出 tempValue:0; i: 3
System.out.println("tempValue: " + tempValue + "; i : " + i.get());
// 取值,然后自增
tempValue = i.getAndIncrement();
// 输出 tempValue:3; i: 4
System.out.println("tempValue: " + tempValue + "; i : " + i.get());
// 取值,然后增加5
tempValue = i.getAndAdd(5);
//输出 tempvalue:4; i:9
System.out.println("tempValue: " + tempValue + "; i : " + i.get());
//CAS 交换
boolean flag = i.compareAndSet(9, 100);
//输出 flag:true; i:100
System.out.println("flag:" + flag + "; i:" + i.get());
}
}
4.3 数组原子类 AtomicIntegerArray
使用原子的方式更新数组里的某个元素:
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。AtomicIntegerArray 类常用方法,具体如下:
//获取 index=i 位置元素的值
public final int get(int i)
//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndSet(int i, int newValue)
//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndIncrement(int i)
//获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndDecrement(int i)
//获取 index=i 位置元素的值,并加上预期的值
public final int getAndAdd(int delta)
//如果输入的数值等于预期值,则以原子方式将 位置 i 的元素值设置为输入值(update)
boolean compareAndSet(int expect, int update)
//最终将位置 i 的元素设置为 newValue
//lazySet 方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
public final void lazySet(int i, int newValue)
下面是一个数组原子类 AtomicIntegerArray 的使用示例,具体代码如下:
public class AtomicTest2 {
public static void main(String[] args) {
int tempvalue = 0;
//原始的数组
int[] array = { 1, 2, 3, 4, 5, 6 };
//包装为原子数组
AtomicIntegerArray i = new AtomicIntegerArray(array);
//获取第 0 个元素,然后设置为 2
tempvalue = i.getAndSet(0, 2);
//输出 tempvalue:1; i:[2, 2, 3, 4, 5, 6]
System.out.println("tempvalue:" + tempvalue + "; i:" + i);
//获取第 0 个元素,然后自增
tempvalue = i.getAndIncrement(0);
//输出 tempvalue:2; i:[3, 2, 3, 4, 5, 6]
System.out.println("tempvalue:" + tempvalue + "; i:" + i);
//获取第 0 个元素,然后增加一个 delta 5
tempvalue = i.getAndAdd(0, 5);
//输出 tempvalue:3; i:[8, 2, 3, 4, 5, 6]
System.out.println("tempvalue:" + tempvalue + "; i:" + i);
}
}
4.4 AtomicInteger 原理
基础原子类(以 AtomicInteger 为例 )主要通过 CAS 自旋 + volatile 相结合的方案实现,既保障了变量操作的线程安全性,又避免了 synchronized 重量级锁的高开销,使得 Java 程序的执行效率大为提升。
AtomicInteger 源码中的主要方法,都是通过 CAS 自旋实现的。CAS 自旋的主要操作为:如果一次 CAS 操作失败,则获取最新的 value 值后,再次进行 CAS 操作,直到成功。
另外,AtomicInteger 所包装的内部 value 成员,是一个使用关键字 volatile 修饰的内部成员。关键字 volatile 的原理比较复杂,可以查看深入解析volatile关键字,简单的说,该关键字可以保证任何线程在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。
4.5 对象操作的原子性之引用类型原子类
引用类型原子类包括以下三种:
- AtomicReference:基础的引用原子类
- AtomicStampedReference:带印戳的引用原子类
- AtomicMarkableReference:带修改标志的引用原子类
上面三种提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。
public class AtomicReferenceTest {
public static void main(String[] args) {
//包装的原子对象
AtomicReference userRef = new AtomicReference();
//待包装的 User 对象
User user = new User("1", "张三");
//为原子对象设置值
userRef.set(user);
System.out.println(" userRef is:" + userRef.get());
//要使用 CAS 替换的 User 对象
User updateUser = new User("2", "李四");
//使用 CAS 替换
boolean success = userRef.compareAndSet(user, updateUser);
System.out.println(" cas result is:" + success);
System.out.println(" after cas,userRef is:" + userRef.get());
}
}
class User implements Serializable {
//用户 ID
String uid;
//昵称
String nickName;
//年龄
public volatile int age;
public User(String uid, String nickName) {
this.uid = uid;
this.nickName = nickName;
}
@Override
public String toString()
{
return "User{" +
"uid='" + getUid() + '\'' +
", nickName='" + getNickName() + '\''+
'}';
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
输出如下:
userRef is:User{uid='1', nickName='张三'}
cas result is:true
after cas,userRef is:User{uid='2', nickName='李四'}
以上代码首先创建了一个 User 对象,然后把 User 对象包装到一个 AtomicReference 类型的引用 userRef 中,如果要修改 userRef 的包装值,需要调用 compareAndSet 方法才能完成。该方法就是通过 CAS 操作 userRef,从而保证操作的原子性。
需要注意的是:使用原子引用类型 AtomicReference 包装了 User 对象之后,只能保障 User 引用的原子操作,对被包装的 User 对象的字段值修改时不能保证原子性的,这点要切记。
4.6 对象操作的原子性之属性更新原子类
如果需要保障对象某个字段(或者属性)更新操作的原子性,需要用到属性更新原子类。属性更新原子类有以下三个:
- AtomicIntegerFieldUpdater:保障整形字段的更新操作的原子性。
- AtomicLongFieldUpdater:保障长整形字段的更新操作的原子性。
- AtomicReferenceFieldUpdater:保障引用字段的更新操作的原子性。
以 AtomicIntegerFieldUpdater 为例来介绍。使用属性更新原子类去保障属性安全更新的流程,大致需要两步:
第一步,更新的对象属性必须使用 public volatile 修饰符。
第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater( )创建一个更新器,并且需要设置想要更新的类和属性。
public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) {
//使用静态方法 newUpdater( )创建一个更新器 updater
AtomicIntegerFieldUpdater updater=
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("1", "张三");
//使用属性更新器的 getAndIncrement、getAndAdd 增加 user 的 age 值
System.out.println(updater.getAndIncrement(user));// 1
System.out.println(updater.getAndAdd(user, 100));// 101
//使用属性更新器的 get 获取 user 的 age 值
System.out.println(updater.get(user));// 101
}
}
User类使用的和AtomicReference使用的User是同一个。
运行结果如下:
0
1
101
5. 结合原子类解决ABA问题
5.1 使用 AtomicStampedReference 解决 ABA 问题
参考乐观锁的版本号,JDK 的提供了一个类似 AtomicStampedReference 类来解决 ABA 问题。AtomicStampReference 在 CAS 的基础上增加了一个 Stamp 印戳(或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference 的 compareAndSet 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳(Stamp标志的值更新为给定的更新值。
AtomicStampReference 的构造器有两个参数,具体如下:
//构造函数,V 表示要引用的原始数据,initialStamp 表示最初的版本印戳(版本号)
AtomicStampedReference(V initialRef, int initialStamp)
AtomicStampReference 的常用的几个方法如下:
//获取被封装的数据
public V getRerference();
//获取被封装的数据的版本印戳
public int getStamp();
AtomicStampedReference 的 CAS 操作的定义如下:
public boolean compareAndSet(
V expectedReference,//预期引用值
V newReference, //更新后的引用值
int expectedStamp, //预期印戳(Stamp)标志值
int newStamp) //更新后的印戳(Stamp)标志值
compareAndSet 方法的第一个参数是原来的 CAS 中原来参数,第二个参数是要替换后的新参数,第三个参数是原来 CAS 数据旧的版本号,第四个参数表示替换后的新参数版本号。进行 CAS 操作时,只有当前引用值等于预期引用值,并且当前印戳值等于预期印戳值,则以原子方式将引用值和印戳(Stamp)值更新为给定的更新值。
下面是一个简单的 AtomicStampedReference 使用示例,通过两个线程分别带上印戳更新同一个 atomicStampedRef 实例的值,第一个线程会更新成功,而第二个线程更新失败,具体代码如下:
public class AtomicStampedReferenceTest{
// 定义原子计数器,初始值 = 100
private static AtomicInteger atomicI = new AtomicInteger(100);
// 定义AtomicStampedReference:初始化时需要传入一个初始值和初始时间
private static AtomicStampedReference asRef = new AtomicStampedReference(100, 0);
/**
* 未使用AtomicStampedReference线程组:TA TB
*/
private static Thread TA = new Thread(() -> {
System.err.println("未使用AtomicStampedReference线程组:[TA TB] >>>>");
// 更新值为101
boolean flag = atomicI.compareAndSet(100, 101);
System.out.println("线程TA:100 -> 101.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
// 更新值为100
flag = atomicI.compareAndSet(101, 100);
System.out.println("线程TA:101 -> 100.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
});
private static Thread TB = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = atomicI.compareAndSet(100, 888);
System.out.println("线程TB:100 -> 888.... flag:" + flag + ",atomicINewValue:" + atomicI.get() + "\n\n");
});
/**
* 使用AtomicStampedReference线程组:T1 T2
*/
private static Thread T1 = new Thread(() -> {
System.err.println("使用AtomicStampedReference线程组:[T1 T2] >>>>");
// 更新值为101
boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("线程T1:100 -> 101.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
// 更新值为100
flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("线程T1:101 -> 100.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
});
private static Thread T2 = new Thread(() -> {
int time = asRef.getStamp();
System.out.println("线程休眠前Time值:" + time);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = asRef.compareAndSet(100, 888, time, time + 1);
System.out.println("线程T2:100 -> 888.... flag:" + flag + ".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
});
public static void main(String[] args) throws InterruptedException {
TA.start();
TB.start();
TA.join();
TB.join();
T1.start();
T2.start();
}
}
/**
* 未使用AtomicStampedReference线程组:[TA TB] >>>>
* 线程TA:100 -> 101.... flag:true,atomicINewValue:101
* 线程TA:101 -> 100.... flag:true,atomicINewValue:100
* 线程TB:100 -> 888.... flag:true,atomicINewValue:888
*
*
* 使用AtomicStampedReference线程组:[T1 T2] >>>>
* 线程休眠前Time值:0
* 线程T1:100 -> 101.... flag:true.... asRefNewValue:101.... 当前Time:1
* 线程T1:101 -> 100.... flag:true.... asRefNewValue:100.... 当前Time:2
* 线程T2:100 -> 888.... flag:false.... asRefNewValue:100.... 当前Time:2
*/
我们观察如上Demo中AtomicInteger与AtomicStampedReference的测试结果可以得知,AtomicStampedReference确实能够解决我们在之前阐述的ABA问题,那么AtomicStampedReference究竟是如何ABA问题的呢?我们接下来再看看它的内部实现:
public class AtomicStampedReference {
// 通过Pair内部类存储数据和时间戳
private static class Pair {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
// 存储数值和时间的内部类
private volatile Pair pair;
// 构造方法:初始化时需传入初始值和时间初始值
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
compareAndSet方法源码实现:
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
// 最终实现调用Unfase类中的compareAndSwapObject()方法
private boolean casPair(Pair cmp, Pair val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
5.2 使用 AtomicMarkableReference 解决 ABA 问题
AtomicMarkableReference 是 AtomicStampedReference 的简化版,不关心修改过几次,仅仅关心是否修改过。因此其标记属性 mark 是 boolean 类型,而不是数字类型,标记属性 mark 仅记录值是否有过修改。
AtomicMarkableReference 适用只要知道对象是否有被修改过,而不适用于对象被反复修改的场景。
public class AtomicMarkableReferenceTest {
static AtomicMarkableReference amRef = new AtomicMarkableReference(100, false);
private static Thread t1 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T1:修改前标志 Mrak:" + mark + "....");
// 将值更新为200
System.out.println("线程T1:100 --> 200.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 200, mark, !mark));
});
private static Thread t2 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T2:修改前标志 Mrak:" + mark + "....");
// 将值更新回100
System.out.println("线程T2:200 --> 100.... 修改后返回值 Result:" + amRef.compareAndSet(amRef.getReference(), 100, mark, !mark));
});
private static Thread t3 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T3:休眠前标志 Mrak:" + mark + "....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = amRef.compareAndSet(100, 500, mark, !mark);
System.out.println("线程T3: 100 --> 500.... flag:" + flag + ",newValue:" + amRef.getReference());
});
public static void main(String[] args) throws InterruptedException {
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
/**
* 输出结果如下:
* 线程T1:修改前标志 Mrak:false....
* 线程T1:100 --> 200.... 修改后返回值 Result:true
* 线程T2:修改前标志 Mrak:true....
* 线程T2:200 --> 100.... 修改后返回值 Result:true
* 线程T3:休眠前标志 Mrak:false....
* 线程T3: 100 --> 500.... flag:true,newValue:500
*/
/* t3线程执行完成后结果还是成功更新为500,代表t1、t2
线程所做的修改操作对于t3线程来说还是不可见的 */
}
}
6. CAS 操作在 JDK 中的应用
CAS 在 java.util.concurrent.atomic 包中的原子类、Java AQS 以及显示锁、CurrentHashMap 等重要并发容器类的实现上,都有非常广泛的应用。
在 java.util.concurrent.atomic 包的原子类如 AtomicXXX 中,都使用了 CAS 保障对数字成员进行操作的原子性。
java.util.concurrent 的大多数类(包括显示锁、并发容器)都基于 AQS 和 AtomicXXX 实现,而 AQS 通过 CAS 保障其内部双向队列头部、尾部操作的原子性。
参考来源:
《Java并发编程的艺术》
《深入理解Java虚拟机》
《Java高并发程序设计》
《Java并发编程实战》
《Java并发编程之美》