一、定义
CAS(compare and swap
),是一种多线程无锁的编程方式。
当修改多个线程可能同时操作的属性时,给定
期望值
和要更新的值
,如果当前值
与期望值
一致,则更新为要设置的值
CAS的底层是通过将读取-比较-设置
三个操作作为一个指令执行,在执行期间不会有其他线程改变变量。这保证了操作的原子性。
CAS其以乐观的态度进行操作,不断循环等待进行,对比而言,线程切换需要 8万个cup时钟周期,而循环重试只需要 几个cup时钟。
CAS需要三个参数:
- offset(内存地址)
- expect(期望值)
- update(需要更新的值)
操作成功返回true,否则返回false。
二、测试并使用
1. synchronized
public class SynchronizedTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
final Object lock = new Object();
Thread[] ths = new Thread[10000];
for(int i=0;i<ths.length;i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(int i=0;i<10000;i++) {
synchronized (lock) {
count ++;
}
}
}
});
ths[i] = thread;
}
long begin = System.currentTimeMillis();
for(int i = 0; i < ths.length; i++) {
ths[i].start();
ths[i].join();
}
System.out.println(count);// 100000000
System.out.println(System.currentTimeMillis() - begin); //4073
}
}
2. cas
public class CASTest {
static final AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread ths [] = new Thread[10000];
for(int i=0;i<ths.length;i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(int i=0;i<10000;i++) {
count.incrementAndGet();
}
}
});
ths[i] = thread;
}
long begin = System.currentTimeMillis();
for(int i = 0; i < ths.length; i++) {
ths[i].start();
ths[i].join();
}
System.out.println(count.get());// 100000000
System.out.println(System.currentTimeMillis() - begin); // 1785
}
}
3. 由此可看出
CAS 性能要比 Synchronized
快上 2.8倍
三、问题
(1) 循环时间过长
若一直不成功, 则会一直尝试, 时间过长, 可能会给CPU带来很大的开销
(2) 只能保证一个共享变量的原子操作
只针对单一变量, 有其局限性
(3) ABA 问题
假如链表为: head->A->B->C
,线程t1要将head->B,cas(A,B)
,此过程中线程t2进行了head->A->C->D,此时B已经处于游离状态,B.next=null,切换到线程t1,t1发现header还是A,就进行交换
head->B
,这时链表丢失了C和D。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个功能。
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0); //初始值为100,初始时间戳为0
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1); //cas 还要判断时间戳
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
四、参考资料
- https://zhuanlan.zhihu.com/p/34556594
- <<码出高效>>