文章目录
CAS介绍
什么是 CAS
CAS(Campare And Swap,比较并交换),通常指一种原子操作:针对一个变量,首先比较它的内存值与期望值是否相同,相同就给它赋一个新值。
CAS的逻辑用伪代码描述如下:
if (value == expectedValue) {
value = newValue;
上面的伪代码描述了一个由比较和复制的两个阶段组成的复合操作,实际上,CAS可以看成是他们的一个整合体,一个不可分割的原子操作,由硬件层面来保障原子性。
CAS可以看做是乐观锁的一种实现,我们Java中的原子中的递增操作就通过CAS自旋实现的。
CAS是一种无锁算法,在没锁的情况下,也就是不阻塞线程的情况下,实现多线程之间的变量同步。
CAS在Java中的应用
在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作,它们都是native方法,由Java 虚拟机提供具体实现
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这里我们以compareAndSwapInt方法为例,该方法接收四个参数,分别表示:对象实例,内存地址便宜量,字段期望值,字段新值。该方法会针对指定对象实例中的相应偏移量的字段进行CAS操作。
代码示例:
public class CASDemo {
public static void main(String[] args) {
Entity entity = new Entity();
Unsafe unsafe = UnsafeFactory.getUnsafe();
// 获取Entity对象字段x的地址偏移量
long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
System.out.println(offset);
boolean successful;
// 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段更新值
successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 5);
System.out.println(successful + "\t" + entity.x);
successful = unsafe.compareAndSwapInt(entity, offset, 3, 8);
System.out.println(successful + "\t" + entity.x);
}
}
class Entity{
int x;
}
上述代码示例运行结果:
12
true 3
true 5
false 5
针对Entity的x属性进行3次CAS操作,分别尝试将x从0改成3,从3改成5,从3改成8。最后一次执行前,x的值为5,期望值3和内存值不相等,执行失败,返回false。
UnsafeFactory.java
public class UnsafeFactory {
/**
* 通过反射获取Unsafe对象
* @return
*/
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe)theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取指定字段偏移量
* @param unsafe
* @param clazz
* @param fieldName
* @return
*/
public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
try {
return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
}
CAS缺陷
CAS虽然高效地解决了原子操作,但是也存在着一些缺陷,主要表现在三个方面:
- 自选CAS长时间不成功,给CPU带来非常大的开销
- 只能保证一个共享变量的原子操作
- ABA问题
ABA问题及其解决方案
CAS算法实现是,从内存中读取某时刻的数据,下一刻比较并替换,在这之间就会存在一个时间差,就会发生ABA问题。
什么是ABA问题
当多个线程对一个原子类进行操作时,某个线程在短时间内将原子类的值A修改为B,立马又将其修改回A,其他线程是无法感知的,会任务原子类没有发生过变化,CAS操作还是能够成功。
代码演示:
@Slf4j
public class ABADemo {
public static void main(String[] args) {
// 原值10
AtomicInteger atomicInteger = new AtomicInteger(10);
new Thread(()->{
int value = atomicInteger.get();
log.debug("ThreadA read value: " + value);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
// ThreadA 通过cas修改value为30
if (atomicInteger.compareAndSet(value, 30)) {
log.debug("ThreadA update from " + value + " to 30");
} else {
log.debug("ThreadA update fail!");
}
}, "ThreadA").start();
new Thread(()->{
int value = atomicInteger.get();
log.debug("ThreadB read value: " + value);
// ThreadB 通过cas修改value为20
if (atomicInteger.compareAndSet(value, 20)) {
log.debug("ThreadB update from " + value + " to 30");
// 其他业务
value = atomicInteger.get();
// ThreadB通过CAS修改value值为10
if (atomicInteger.compareAndSet(value, 10)) {
log.debug("ThreadB update from " + value + " to 10");
}
}
}, "ThreadB").start();
}
}
运行结果:
[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB read value: 10
[ThreadA] DEBUG org.example.juc.automic.ABADemo - ThreadA read value: 10
[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB update from 10 to 30
[ThreadB] DEBUG org.example.juc.automic.ABADemo - ThreadB update from 20 to 10
[ThreadA] DEBUG org.example.juc.automic.ABADemo - ThreadA update from 10 to 30
从运行结果就可以看出,ThreadA不清楚ThreadB对共享变量进行过操作,认为value没有修改过
ABA问题的解决方案
数据库中有一种乐观锁,是一种基于数据版本实现数据同步的机制,没修改一次数据,版本就会进行累加。
同样,Java中也有类似的版本解决方案:原子引用类AtomicStampedReference
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
...
}
reference即我们实际存储的变量,stamp是版本,每次修改可以通过+1保证版本唯一性。这样就可以保证每次修改后的版本也会往上递增。
常用方法
传入一个int类型数组,返回我们当前内存实际存储的变量reference,并将传入的int数组第0位元素写入当前版本号
public V get(int[] stampHolder)
第一个参数期望值,第二个参数新值,第三个参数期望版本号,第四个参数为更新成功后的版本号。
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp,int newStamp)
代码测试:
@Slf4j
public class AtomicStampedReferenceDemo {
public static void main(String[] args) {
// 定义AtomicStampedReference Pair.reference值为1, Pair.stamp为1
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(10,1);
new Thread(()->{
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
log.debug("ThreadA read value: " + value + ", stamp: " + stamp);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
// ThreadA 通过cas修改value为30
if (atomicStampedReference.compareAndSet(value, 30, stamp, stamp+1)) {
log.debug("ThreadA update from " + value + " to 30");
} else {
log.debug("ThreadA update fail!");
}
}, "ThreadA").start();
new Thread(()->{
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
log.debug("ThreadB read value: " + value + ", stamp: " + stamp);
// ThreadB 通过cas修改value为20
if (atomicStampedReference.compareAndSet(value, 20, stamp, stamp + 1)) {
log.debug("ThreadB update from " + value + " to 20");
// 其他业务
value = (int) atomicStampedReference.get(stampHolder);
stamp = stampHolder[0];
log.debug("ThreadB read value: " + value+ ", stamp: " + stamp);
// ThreadB通过CAS修改value值为10
if (atomicStampedReference.compareAndSet(value, 10, stamp, stamp + 1)) {
log.debug("ThreadB update from " + value + " to 10");
}
}
}, "ThreadB").start();
}
}
运行结果:
[ThreadA] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadA read value: 10, stamp: 1
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB read value: 10, stamp: 1
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB update from 10 to 20
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB read value: 20, stamp: 2
[ThreadB] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadB update from 20 to 10
[ThreadA] DEBUG org.example.juc.automic.AtomicStampedReferenceDemo - ThreadA update fail!
ThreadA修改失败。
Atomic原子操作类介绍
在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。
在java.util.concurrent.atomic包里提供了一组原子操作类:
基本类型:AtomicInteger、AtomicLong、AtomicBoolean;
引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
原子类型累加器:DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64(jdk1.8增加的类)
原子类使用
基本类型原子类使用
以AtomicInteger为例总结常用的方法:
getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值
public final int getAndIncrement()
getAndSet(int newValue):将实例中的值更新为新值,并返回旧值
public final boolean getAndSet(boolean newValue)
incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果
public final int incrementAndGet()
addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果
public final int addAndGet(int delta)
代码示例:
public class AtomicIntegerDemo {
static AtomicInteger sum = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// 原子自增 CAS
sum.incrementAndGet();
//TODO
}
});
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum.get());
}
}
运行结果:
100000
我们看下它的JDK源码:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* 通过do-while循环自选的方式来实现原子自增。
*
* 首先获取字段的内存值,然后通过CAS操作更新字段值,
* 更新失败后继续执行循环体语句获取新的内存值,
* 更新成功后返回更新前的内存值,通过上层方法返回更新后的值。
*
* @param var1 对象实例,即AtomicInteger的实例
* @param var2 AtomicInteger对象中value字段的偏移量
* @param var4 自增1
* @return
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
从源码中可以看到,原子类自增操作是自旋 + CAS操作来实现。
这种CAS失败自选的操作存在什么问题?
长时间自选,会占用CPU资源。
数组类型原子类使用
AtomicIntegerArray为例总结常用的方法:
addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加
public final int addAndGet(int i, int delta)
getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1
public final int getAndIncrement(int i)
compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新
public final boolean compareAndSet(int i, int expect, int update)
代码示例:
public class AtomicIntegerArrayDemo {
static int[] value = new int[]{ 1, 2, 3, 4, 5 };
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
public static void main(String[] args) throws InterruptedException {
//设置索引0的元素为100
atomicIntegerArray.set(0, 100);
System.out.println(atomicIntegerArray.get(0));
//以原子更新的方式将数组中索引为1的元素与输入值相加
atomicIntegerArray.getAndAdd(1,5);
System.out.println(atomicIntegerArray);
}
}
运行结果:
100
[100, 7, 3, 4, 5]
引用类型原子类使用
AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性。