CAS全称compare and swap,是“比较并替换”的意思。CAS有3个重要的操作数:内存值(a)、预期值(aExpe)、更新值(aNew),当且仅当预期值与内存值相同,才将内存值改为修改值,否则什么都不做,最后返回成功或失败。下图是“比较并替换”的执行流程。
使用synchronized来模拟“比较并替换”的代码是这样的
/**
* 使用代码模拟CAS
*/
class SimulatedCAS{
private volatile int value;
public synchronized int compareAndSwap(int expectedValue, int newValue){
int oldValue = value;
if (oldValue == expectedValue){
value = newValue;
}
return oldValue;
}
}
CAS与synchronized不同之处在于,synchronized是一种悲观锁,悲观锁假设了多个线程在同一时刻修改共享数据的情况一定会发生,所以给访问共享数据的代码加上锁。而CAS能实现乐观锁,乐观锁认为每次去拿共享数据的时候认为其他线程不会修改此数据,所以不会上锁,在更新共享数据的时候判断在此期间别的线程有没有修改共享数据,数据被修改,则失败,数据未被修改则更新成功。
如果“比较并替换”失败了,该怎么处理呢?以上图中的线程2位例子,线程2需要重新获取数据并执行“比较并替换”操作,可使用一个do while循环实现,这个do while循环也称为自旋,比方说因为while的条件在短时间内都是true,线程反复执行do while的代码,类似于自我旋转。
大家应该都知道,在Java中,i++不是原子操作,在多线程下是不安全的,可以使用AtomicInteger的getAndAdd(int delta)实现线程安全的数字自增。AtomicInteger#getAndAdd(int delta)调用了Unsafe.getAndAddInt(Object var1, long var2, int var4)方法,以下是Unsafe.getAndAddInt(Object var1, long var2, int var4)方法源码的源码,使用了CAS和自旋。
/**
* Unsafe#getAndAddInt(java.lang.Object, long, int)方法源码
*
* @param var1 可能是原子操作类对象,例如AtomicInteger
* @param var2 共享变量在内存中的偏移地址
* @param var4 自增的数值
* @return
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 通过内存偏移地址取共享变量的值
var5 = this.getIntVolatile(var1, var2);
/**
* 在一次原子操作中完成“比较并替换”
* 再次通过var1、var2获取共享变量在内存中的值,如果仍然等于var5,则将共享变量设置为var5+var4
* “比较并替换”失败,则循环重试,直到成功
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Unsafe是Java实现CAS的核心类,此类提供了硬件级别的原子操作,很多方法是native方法,即方法不是Java语言实现,可能是由C/C++实现,Java可调用这些方法。
AtomicInteger实现线程安全的自增代码演示
class AtomicIntegerDemo{
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static int num = 0;
private static Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
incrementAtomic();
increment();
}
};
/**
* 原子类递增
*/
private static void incrementAtomic(){
atomicInteger.getAndAdd(1);
}
/**
* ++ 方式递增
*/
private static void increment(){
num++;
}
public static void main(String[] args) throws Exception{
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
Thread t4 = new Thread(runnable);
Thread t5 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
System.out.println("原子类结果:"+atomicInteger.get());
System.out.println("普通变量结果:"+ num);
}
}
CAS有以下缺点:
1、线程1获取到共享变量值是“A”,进行一系列操作,在此期间,线程2将变量改成“B”,线程3再将变量改为“A”,线程1执行到设置值阶段,认为值没有被修改,还是A。这也称为ABA问题。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2、循环时间长开销大。如果CAS失败,则会进行自旋,长时间自旋很消耗CPU。可以设置一个自旋阀值N,尝试N次失败后转为阻塞锁,并且N能够在程序运行过程中动态调整。这称为自适应自旋。
悲观锁适合场景:
1、并发写请求多的情况
2、适用于临界区持锁时间比较长的情况,悲观锁可以避免大量无用自旋等的消耗。典型场景:临界区有IO操作、临界区代码复杂或者循环量大。(注:同一时刻只有一个线程能执行的的代码片段叫做临界区)
乐观锁适合场景:
1、大部分是读请求的场景,不加锁能让读取性能大幅度提高。
CAS在ConcurrentHashMap中的应用
在JDK8之前,ConcurrentHashMap使用分段加锁的方式实现,每一段称为一个segment,默认分成16段。到了JDK8后,使用HashMap、synchronized、cas实现线程安全。
class ConcurrentHashMapDemo{
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();
private static Runnable runnable = () -> {
for (int i = 0; i < 1000; i++) {
// 线程安全的写法。自旋 + CAS
Integer score, newScore;
do {
score = scores.get("小明");
newScore = score + 1;
}while (!scores.replace("小明", score, newScore));
///**
// * 组合操作,线程不安全
// * ConcurrentHashMap只是源码内部保证线程安全,使用者要保证自己的代码也是线程安全的
// */
//Integer score = scores.get("小明");
//Integer newScore = score + 1;
//scores.put("小明", newScore);
}
};
public static void main(String[] args) throws Exception{
scores.put("小明", 0);
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(scores);
}
}