在JDK5之前,可以通过synchronized或Lock来保证高并发的业务场景下的线程安全,但是synchronized或Lock都属于互斥锁的方案,互斥锁所带来的比较重量级、加锁、释放锁都会带来性能上的损耗。
于是,就出现了CAS机制实现无锁的解决方案,CAS和乐观锁类似,CAS可以达到非阻塞同步的方式来保证线程安全。
CAS是现代CPU广泛支持的一种对内存中共享数据进行操作的一种特殊指令,这个指令可以对内存中的共享数据做原子的读写操作。
CAS的核心思想就是让CPU比较内存中某个值是否和预期值相同,如果相同则将这个值设置为新值,如果不相同则不做更新操作。
CAS的基本流程如上所示:
上述这些操作都是由CPU指令来保证原子性的。
在CAS底层的实现原理,实际上是通过Unsafe类和自旋锁来实完成的。
在AtomicInteger
源码中,可以看见都是通过Unsafe
类来实现更新的。
Unsafe
类是JDK内部常用工具栏,**它通过暴露一些Java意义上说“不安全”的功能给Java层代码,来让JDK能够更多的使用Java代码来实现一些原本是平台相关的、需要使用native语言(例如C或C++)才可以实现的功能。**该类不应该在JDK核心类库之外使用,这也是命名为Unsafe(不安全)的原因。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 用于获取value字段相对当前对象的“起始地址”的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
//返回当前值
public final int get() {
return value;
}
//递增加detla
public final int getAndAdd(int delta) {
// 1、this:当前的实例
// 2、valueOffset:value实例变量的偏移量
// 3、delta:当前value要加上的数(value+delta)。
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//递增加1
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
...
}
Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()
方法用于获取某个字段相对Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject
之类的方法可以使用前面获取的偏移量来访问某个Java对象的某个字段。在AtomicInteger
的static
代码块中便使用了objectFieldOffset()
方法。
Unsafe类的功能主要分为内存操作、CAS、Class相关、对象操作、数组相关、内存屏障、系统相关、线程调度等功能。这里我们只需要知道其功能即可,方便理解CAS的实现,注意不建议在日常开发中使用。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
上述代码等于是AtomicInteger调用UnSafe类的CAS方法,JVM帮我们实现出汇编指令,从而实现原子操作。
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;
}
在上面的getAndAddInt
方法中三个参数
在getAndAddInt
方法中,会首先将当前对象主内存的值赋值给val5
,然后进入while
循环,判断当前对象此刻主内存中的值是否等于val5
,如果是,那么就更新内存中的值,否则继续循环,重写获取val5
的值(这里是针对上面的incrementAndGet
自增来说的)
这样的话即使有其他线程修改了内存的值,CAS会比对内存值是否和预期值相同从而判断是否要更新内存的值。同时因为上面的compareAndSwapInt
是一个native
方法,这个方法汇编之后是CPU原语指令,原语指令是连续执行不会被打断的,所以可以保证原子性。
像上面getAndAddInt
方法中的do while
循环操作,就是所谓的自旋,如果预期值和主内存的值不一样,则需要重新获取主内存的值,这就是自旋。
CAS虽然解决了多线程下的安全问题,但是存在一个非常明显的缺点,那就是当内存使用自旋进行CAS更新的时候(while循环CAS更细你,如果更新失败,则),如果长时间不成功,对CPU来说将会造成极大的开销。
CAS虽然高效实现了原子性的操作,但是存在下面三个缺点:
前面也提到了Unsafe的实现使用了自旋锁的机制,如果当前CAS操作失败,就需要循环进行CAS,如果长时间不成功,那么就会造成CPU极大的开销。
CAS是一个针对一个共享变量使用的机制,可以保证原子性,但是如果存在多个共享变量,或者需要一整块代码的逻辑都保证线程安全,那么CAS就无法保证原子操作了,这时候就需要考虑使用synchronized或Lock这些重量级锁来保证线程安全了。
或者将多个共享变量合并程一个共享变量从而进行CAS操作。
虽然使用CAS可以实现非阻塞式的原子性操作,但是会产生ABA问题
虽然线程A以为变量值没有改变,继续执行了,但是这个会引发一些潜在的问题。ABA问题最容易发生在lock free
的算法中的,CAS首当其冲,因为CAS判断的是指针的地址。如果这个地址被重用了呢,问题就很大了(地址被重用是很经常发生的,一个内存分配后释放了,再分配,很有可能还是原来的地址)。
ABA问题的解决思路就是使用版本号:在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
从Java 1.5开始,JDK的Atomic
包里提供了一个类AtomicStampedReference
来解决ABA问题。这个类的compareAndSet
方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
参考:一篇搞定CAS,深度讲解,面试实践必备 - 腾讯云开发者社区-腾讯云 (tencent.com)