CAS是什么?有什么作用?
CAS(Compare-and-Swap)是比较并交换
,是旧的预期值和内存地址中的值相等时才会去执行更新操作
,CAS操作用来避免阻塞同步
。
CAS操作,该操作由sun.misc.Unsafe
类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。
CAS的工作原理?
CAS指令需要有三个操作数,
分别是
内存位置
(在Java中可以简单地理解为变量的内存地址,用V表示)、
旧的预期值
(用A表示)
和准备设置的新值
(用B表示)。
CAS指令执行时,只有当变量的预期值A和内存地址V当中的实际值相同时,处理器才会用B更新V的值,否则它就不执行更新。 但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。
如何通过CAS操作避免阻塞同步?
测试的代码如代码清单1所示。这段代码里我们曾经通过20个线程自增10000次的操作来证明volatile变量不具备原子性,那么如何才能让它具备原子性呢?
方案之一是把race++操作或increase()方法用同步块包裹起来
,这是使用锁的解决方案,如:代码清单2
但是如果改成AtomicInteger原子自增运算 如:代码清单3所示的写法,效率将会提高许多。
代码清单1 volatile的运算
/**
* volatile变量自增运算测试
*
* @author zzm
*/
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
// 等待所有累加线程都结束
while (Thread.activeCount() > 2)
Thread.yield();
System.out.println(race);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
执行结果:
186665
耗时:15
代码清单2 synchronized包裹increase()方法的volatile的运算
public class VolatileTest {
public static volatile int race = 0;
public synchronized static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
// 等待所有累加线程都结束
while (Thread.activeCount() > 2)
Thread.yield();
System.out.println(race);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
执行结果:
200000
耗时:40
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高
。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”
。
CAS的应用
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
代码清单3 Atomic的原子自增运算
/**
* Atomic变量自增运算测试
*
* @author zzm
*/
public class AtomicTest {
public static AtomicInteger race = new AtomicInteger(0);
public static void increase() {
race.incrementAndGet();
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[THREADS_COUNT];
long start = System.currentTimeMillis();
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 2)
Thread.yield();
System.out.println(race);
System.out.println("耗时:" + (System.currentTimeMillis() - start));
}
}
执行结果:
200000
耗时:19
使用AtomicInteger代替int后,程序输出了正确的结果,而Atomic操作类的底层正是用到了“CAS机制”
。
来看一下incrementAndGet()方法的原子性:
这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:
- 获取当前值
- 当前值+1,计算出目标值
- 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
如代码清单4所示。
代码清单4 incrementAndGet()方法的JDK源码
/**
* Atomically increment by one the current value.
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
继续查看compareAndSet()方法的源码如下,使用的是sun.misc.Unsafe中的方法来实现的
代码清单5 compareAndSet()方法的JDK源码
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
从思想上来说,synchronized属于悲观锁
,悲观的认为程序中的并发情况严重,所以严防死守,
CAS属于乐观锁
,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
CAS的缺点?
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用锁了。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
3) ABA问题
CAS存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?
这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。
怎么解决ABA问题呢?
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
真正要做到严谨的CAS机制
,我们在compare阶段不仅要比较期望值A和地址V中的实际值
,还要比较变量的版本号是否一致
。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference类就实现了用版本号作比较额CAS机制。
这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
拓展总结:Java中可以通过什么方式来实现原子操作?
在Java中可以通过锁和循环CAS的方式来实现原子操作。
1、循环CAS的方式
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止(若不成功则CPU也不放弃时间片,循环发起 CAS 操作,直至成功)。
2、使用锁机制
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。
有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS
,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count。
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<Thread>(600);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
// 等待所有线程执行完成
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("非线程安全计数器:" + cas.i);
System.out.println("CAS线程安全计数器:" + cas.atomicI.get());
System.out.println("耗时:" +System.currentTimeMillis() - start);
}
/** * 使用CAS实现线程安全计数器 */
private void safeCount() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
/**
* 非线程安全计数器
*/
private void count() {
i++;
}
}
执行结果:
非线程安全计数器: 959014
CAS线程安全计数器: 1000000
耗时: 90
CAS在虚拟机中的应用?
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,
可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决方案:对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
;
CAS 性能优化
大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?
Java8 有一个新的类,LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:
参考:深入理解java虚拟机 —— 周志明
文章:什么是CAS机制?
文章:什么是乐观锁,什么是悲观锁