CAS算法(Compare And Swap),即比较并替换,是一种实现并发编程时常用到的算法,Java并发包中的很多类都使用了CAS算法。
CAS算法有3个基本操作数:
CAS使用自旋的方式来交换值,操作步骤为:
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
对于互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。
自旋锁的实现基础是CAS算法机制。CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。
先看一个线程不安全的例子:
public class AutomicDemo {
public static int num = 0;
public static void main(String[] args){
for(int i=0; i<5; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j=0; j<200; j++){
num++;
}
}
}).start();
}
/* 主控失眠3秒,保证所有线程执行完成 */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num=" + num);
}
}
输出结果:
num=950
因为自增操作不是原子性,多线程环境下,访问共享变量线程不安全。
解决方法,加synchronized同步锁:
for(int j=0; j<200; j++){
synchronized (AutomicDemo.class){
num++;
}
}
输出结果:
num=1000
线程安全。
synchronized确保了线程安全,但会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。
于是JDK提供了一系列原子操作类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,它们都是基于CAS去实现的,下面我们就来详细看一看原子操作类。
public class AutomicDemo {
public static AtomicInteger num = new AtomicInteger(0);
public static void main(String[] args){
for(int i=0; i<5; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j=0; j<200; j++){
num.incrementAndGet();
}
}
}).start();
}
/* 主控失眠3秒,保证所有线程执行完成 */
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("num=" + num.get());
}
}
输出:
num=1000
使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比Synchronized更好。
所有Atomic相关类的实现都是通过CAS(Compare And Swap)去实现的,它是一种乐观锁的实现。
CAS实现放在 Unsafe 这个类中的,其内部大部分是native方法。这个类是不允许更改的,而且也不建议开发者调用,它只是用于JDK内部调用,看名字就知道它是不安全的,因为它是直接操作内存,稍不注意就可能把内存写崩。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力。有一下特点:
1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
去看AtomicInteger的内部实现可以发现,全是调用的Unsafe类中的方法:
该文章详细介绍所有原子类,后续有时间整理学习
https://blog.csdn.net/fanrenxiang/article/details/80623884
ABA问题:CAS算法通过比较变量值是否相同来修改变量值以保证原子性,但如果一个内存地址的值本身是A,线程1准备修改为C。在这期间,线程2将值修改为B,线程3将值修改为A,线程1获取内存地址的值还是A,故修改为C成功。但获取的A已不再是最开始那一个A。这就是经典的ABA问题,A已不再是A。
如果解决ABA问题呢?
两个方法,1、增加版本号;2、增加时间戳。
Java原子类使用自旋的方式来处理每次比较不相同时后的重试操作,下面来看看AtomicInteger类incrementAndGet方法的代码:
//AtomicInteger 的incrementAndGet方法,变量U为静态常量jdk.internal.misc.Unsafe类型
public final int incrementAndGet() {
//使用getAndAddInt方法,实际操作类似j++
return U.getAndAddInt(this, VALUE, 1) + 1;
}
//jdk.internal.misc.Unsafe类型的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取变量o的可见值
v = getIntVolatile(o, offset);
//比较与替换变量o(CAS算法)
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
//jdk.internal.misc.Unsafe类型的weakCompareAndSetInt方法
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
//执行比较与替换
return compareAndSetInt(o, offset, expected, x);
}
在Unsafe类的getAndAddInt方法中使用了do…while循环语句,循环语句的作用为自旋,Unsafe的weakCompareAndSetInt实现CAS算法。
如果weakCompareAndSetInt一直不成功将会一直自旋下去,这将消耗过多的CPU时间。而且原子类使用CAS算法实现,这导致原子类只能保证一个变量的原子操作,对于需要保证一个具有多个操作的事务将变得无能为力。
总结如下:
为应对CAS存在缺点,替换方案如下:
参考文章:
漫画:什么是 CAS 机制?
知识点: JAVA 悲观锁与乐观锁原理分析 ABA与自旋效率问题分析及解决