乐观的并发策略——CAS非阻塞操作

什么是CAS?
CAS(Compare-and-Swap,比较并交换)操作是一种乐观的并发策略。

CAS 操作需要三个操作数 , 内存位置(V)、预期值(A)和新值(B)。执行CAS操作时,如果内存位置的值与预期值相等,处理器就会用新值(B)更新V的值。否则,处理器不做更新。无论是否更新了V的值,都会将V的旧值返回。

上面的比较并交换的过程,虽然从语义上来看是多次操作,但事实上,它的操作是原子操作,不会被打断。这是由硬件来保证的(利用CPU的CAS指令,同时使用JNI来完成非阻塞算法,相比于独占锁效率会更好)。比如X86可以通过cmpxchg指令来完成CAS操作。

CAS无锁算法和阻塞同步
java程序员接触最多的阻塞同步应该是synchronized 了,它是一种互斥的同步机制,同一时刻只能有一个线程进入临界区,只有当该线程从临界区出来,释放了互斥锁后其他线程才能进入。这是一种悲观的并发策略,无论共享数据是否出现竞争都要进行加锁。而线程的唤醒和阻塞是比较消耗性能的,因此synchronized 算是比较重的操作(当然JDK1.6后已经对synchronized 进行了很多优化,会从偏向锁逐渐膨胀到重量级锁,而不是上来就是重量级锁)。而CAS操作有别于synchronized 之处在于它不会阻塞线程,如果现在有多条线程对共享变量进行更新,只有一个线程会成功,其他线程都会失败,失败的线程会不断重试,直到成功。这也是为什么说CAS是一种乐观的并发策略的原因,CAS操作是先进行更新,如果没有其他线程对共享变量进行操作就成功。如果有就失败,失败了再采取补救措施(不断重试,直到成功)。

我们如何使用CAS操作
在JDK1.5后,java给我们提供了sun.misc.Unsafe类,这个类中的一系列方法提供了对CAS操作的支持,不可可惜的是我们不能直接使用它,只能间接的通过java提供的一系列Atom原子类来进行CAS操作,这些原子类内部使用的就是Unsafe类提供的CAS操作。

现在看看AtomicInteger如何实现CAS操作的。
Example:

public class Main {

    static int a=0;
    static AtomicInteger atomicInteger=new AtomicInteger(0);
    static CountDownLatch latch=new CountDownLatch(3);
    public static void main(String[] args) throws InterruptedException {

        ExecutorService service= Executors.newCachedThreadPool();

        for (int i=0;i<3;i++){
            service.submit(new Runnable() {
                @Override
                public void run() {
                    for (int j=0;j<1000;j++){
                        a++;
                        atomicInteger.getAndIncrement();
                    }
                    latch.countDown();
                }
            });
        }
        latch.await();

        System.out.println("a="+a);
        System.out.println("atomicInteger="做了啥+atomicInteger.get());
    }

}

上面代码演示了两个共享变量a、atomicInteger在多线程并发的情况下自增的结果。我们知道变量a的自增不是原子性的,并且对其他线程也不是立即可见的,因此结果肯定有问题。那atomicInteger呢?

a=2881
atomicInteger=3000

多次执行a的结果都不同,但是atomicInteger和我们预期结果一致,这是因为atomicInteger的自增内部使用了Unsafe类的CAS操作。点进入看看getAndIncrement方法做了啥?

AtomicInteger#getAndIncrement
 public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

可以看到内部确实调用了unsafe提供的方法。

Unsafe#getAndAddInt
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

var2就是内存位置(V),var5是预期值,var5 + var4代表新值,在getAndAddInt方法中首先获取旧值作为预期值,然后调用compareAndSwapInt进行CAS操作,判断内存位置的值与预期值是否相等,如果相等就用新值更新V的值。如果不想等就重试,直到成功。
比如说现在有多个线程进入getAndAddInt方法,线程A,B获取到内存位置的旧值var5,但是A线程先一步调用了compareAndSwapInt方法更新了内存位置的值,线程B再调用compareAndSwapInt的时候就会发现内存位置的值和var5不相等了,说明有其他线程更新了,那自己只好重试了。
注意:“比较+更新”操作是封装在compareAndSwapInt()中,它是借助于一个CPU指令完成的,属于原子操作,文章开头已经解释过了。

CAS的不足

  1. CAS操作只能保证对一个共享变量操作的原子性,因此使用场景有限。
  2. 存在一个逻辑上的Bug,"ABA"问题。当有多个线程对共享变量操作,其中一个线程先将变量从A更新为B,然后又重新更新为A,其他线程进行CAS操作时会认为没有变化,实际上却变化了。当然大部分情况下"ABA"问题不会影响并发程序的正确执行。
  3. CAS操作如果用在复杂的并发场景容易出错,会增加程序的复杂性。
  4. 消耗CPU性能,CAS操作失败会不断的重试,虽然线程不会被挂起,但确是以消耗CPU为代价的,只不过比其阻塞唤醒的代价要小的多。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值