大家碰到了实现一个线程安全的计数器的需求改怎么做呢?根据经验你应该知道我们要在多线程中实现共享变量的原子性和可见性问题,于是锁成为一个不可避免的话题,下文讨论的是与之对应的无锁CAS。
为什么要无锁
我们一想到在多线程下保证安全的方式,肯定是锁,不管从硬件、操作系统层面都或多或少在使用锁。锁有优缺点吗?
使用锁就需要获得锁、释放锁,CPU需要通过上下文切换和调度管理来进行这个操作,对于一个独占锁,一个线程在持有锁后没有执行结束,其他线程就必须等待,等到前面的线程执行完毕,CPU就会把锁拿出来给其他线程来抢了。锁的这种概念基于一种悲观机制,它总是认为数据会被修改,所以,你在操作一部分代码块之前先加一把锁,操作完成后再释放,这样就安全了。
什么是 CAS
比较并交换(compare and swap,CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致性问题。该操作通过将内存中的值与指定的数据进行比较,当数值一样时,将内存中的数据替换成新值。
JAVA如何实现
package com.concurrent.program;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger atomicI = new AtomicInteger(0);
private int i = 0;
public static void main(String[] args) {
final Counter cas = new Counter();
Listts = new ArrayList(10);
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.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++;
}
}
以上代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器方法count,读者可以自行运行,运行结果如下:
非线程安全计数器: 971850
使用CAS实现线程安全计数器: 1000000
执行时间:78ms
CAS存在的问题
1.ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有则更新。但是如果原来一个值是A,变成了B,又变成A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际发生了变化了。ABA解决问题的思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从JAVA1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
2.循环时间长,开销大。CAS长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提示。
3.只能保证一个共享变量的原子操作。对多个共享变量操作时,CAS可以把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并成ij=2a,然后用CAS来操作ij。从JAVA1.5开始,JDK的Atomic包里面提供了一个类AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里面来进行CAS操作。