比较并交换
(Compare-and-Swap,简称称CAS
),CAS操作用来避免阻塞同步
。
CAS指令需要有三个操作数,
分别是内存位置
(在Java中可以简单地理解为变量的内存地址,用V表示)、旧的预期值
(用A表示)和准备设置的新值
(用B表示)。
CAS指令执行时,当且仅当V符合A时,处理器才会用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];
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);
}
}
代码清单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];
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);
}
}
synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高
。
尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”
。
所谓原子操作类,指的是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];
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() > 1)
Thread.yield();
System.out.println(race);
}
}
使用AtomicInteger代替int后,程序输出了正确的结果,而Atomic操作类的底层正是用到了“CAS机制”
。
来看一下incrementAndGet()方法的原子性:
这段代码是一个无限循环,也就是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;
}
}
从思想上来说,synchronized属于悲观锁
,悲观的认为程序中的并发情况严重,所以严防死守,
CAS属于乐观锁
,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
1) CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
2) 不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3) ABA问题
CAS存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?
这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。
加个版本号就可以了。
真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值
,还要比较变量的版本号是否一致
。
在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,
可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决方案:对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
;
参考:深入理解java虚拟机 —— 周志明
文章:什么是CAS机制?