CAS的作用
在多线程并发下,可以通过加锁来保证线程安全性,但多个线程同时请求锁,而线程的挂起和恢复会有很大的开销。一些细粒度的操作,例如同步容器,操作往往只有很少代码量,如果存在锁并且线程激烈地竞争,调度的代价很大。在硬件的支持下,出现了非阻塞的同步机制,其中一种常用实现就是CAS。
CAS说明
CAS(compare and swap,比较并交换)操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B。否则,处理器不做任何操作并返回V值。当多个线程尝试使用CAS同时更新一个变量,最终只有一个线程会成功,其他线程都会失败。但和使用锁不同,失败的线程不会被阻塞,而是被告之本次更新操作失败了,线程可以根据实际情况,继续重试或者跳过操作,大大减少因为阻塞而损失的性能。
Atomic原子类
Atomic原子类是CAS操作的一种体现,它分为基本数据类型原子类、数组类型原子类、引用类型原子类、字段类型原子类,其内部原理都差不多一致。例如AtomicInteger、AtomicReference、AtomicIntegerArray等等。下面以AtomicInteger为例进行简单说明。
public class MyThread implements Runnable{
private AtomicInteger count1 = new AtomicInteger(0);
private int count2 = 0;
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
count1.incrementAndGet();
count2++;
}
}
public void printCount1() {
System.out.println("count1: " + count1.get());
}
public void printCount2() {
System.out.println("count2: " + count2);
}
}
public static void main(String[] args){
MyThread thread = new MyThread();
Thread t1 = new Thread(thread, "thread1");
Thread t2 = new Thread(thread, "thread2");
t1.start();
t2.start();
while(Thread.activeCount()>2) {
Thread.yield();
}
thread.printCount1();
thread.printCount2();
}
main函数中下面的循环是保证上面线程执行完成,然后输出结果。count1 是AtomicInteger ,而count2 是正常的int型。用两个线程一起执行20万次自增,得到的结果如下:
count1: 200000
count2: 168942
在每次执行的时候,count2的最终结果都不一致,而count1却始终是正确的结果。说明了AtomicInteger 是线程安全的。
CAS的局限性
- ABA问题
根据CAS工作的基本原理,存在以下情况,变量初始值为 100,主线程通过 CAS读到了 100,接着来一个线程将这个变量改为 999,之后又一个线程又改成 100 。而轮到主线程发现 a 的值依然是 100,它视作没有人竞争过,于是修改 a 的值。这种情况,虽然 CAS 会更新成功,但是会存在潜在的问题,中途加入的线程的操作对于后一个线程根本是不可见的。而一般的解决办法是为每一次操作加上加时间戳,CAS 不仅关注变量的原始值,还关注上一次修改时间。 - 循环时间的额外开销
CAS 方法一般都定义在一个循环里面,直到修改成功才会退出循环,如果在某些并发量较大的情况下,变量的值始终被别的线程修改,本线程始终在循环里做判断比较旧值,效率低下。所以说,CAS 适用于并发量不是很高的场景。 - 只能保证一个变量的原子操作
CAS 只能对一个变量进行原子性操作,而锁机制则不同,获得锁之后,就可以对所有的共享变量进行修改而不会发生任何问题,因为别人没有锁不能修改这些共享变量。