(转)乐观的并发策略——基于CAS的自旋

摘要:
乐观锁定的核心算法是CAS,它包含三个操作数:内存值、期望值和新值。如果发生非原子性,我们如何保证CAS操作期间并发所导致的问题?此外,您可能会认为CAS通过互斥体实现了原子性,这是事实,但以这种方式确保原子性显示是没有意义的。如果CAS长时间失败并继续运行,将给CPU带来大量开销。乐观锁是对悲观锁的改进。虽然它也有缺点,但它确实已经成为提高并发性能的主要手段。此外,基于CAS的乐观锁广泛用于jdk中的并发包。

  悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题;而乐观者的人生观则相反,凡事不管最终结果如何,他都会先尝试去做,大不了最后不成功。这就是悲观锁与乐观锁的区别,悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。这一节将对乐观锁进行深入探讨。

上节讨论的Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果锁的时间比较长,其性能开销将会变得很大。有没有办法解决这个问题?答案是基于冲突检测的乐观锁。这种模式下,已经没有所谓的锁概念了,每条线程都直接先去执行操作,计算完成后检测是否与其他线程存在共享数据竞争,如果没有则让此操作成功,如果存在共享数据竞争则可能不断地重新执行操作和检测,直到成功为止,可叫CAS自旋。

乐观锁的核心算法是CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。如图2-5-4-1,有两个线程可能会差不多同时对某内存操作,线程二先读取某内存值作为预期值,执行到某处时线程二决定将新值设置到内存块中,如果线程一在此期间修改了内存块,则通过CAS即可以检测出来,假如检测没问题则线程二将新值赋予内存块。

(转)乐观的并发策略——基于CAS的自旋第1张

图2-5-4-1

假如你足够细心你可能会发现一个疑问,比较和交换,从字面上就有两个操作了,更别说实际CAS可能会有更多的执行指令,他们是原子性的吗?如果非原子性又怎么保证CAS操作期间出现并发带来的问题?我是不是需要用上节提到的互斥锁来保证他的原子性操作?CAS肯定是具有原子性的,不然就谈不上在并发中使用了,但这个原子性是由CPU硬件指令实现保证的,即使用JNI调用native方法调用由C++编写的硬件级别指令,jdk中提供了Unsafe类执行这些操作。另外,你可能想着CAS是通过互斥锁来实现原子性的,这样确实能实现,但用这种方式来保证原子性显示毫无意义。下面一个伪代码加深对CAS的理解:

public class AtomicInt {

   private volatile int value;

   public final int get() {

       return value;

    }

publicfinal int getAndIncrement() {

       for (;;) {

           int current = get();

           int next = current + 1;

           if (compareAndSet(current, next))

                return current;

       }

    }

   

   public final boolean compareAndSet(int expect, int update) {

     Unsafe类提供的硬件级别的compareAndSwapInt方法;

    }

}

其中最重要的方法是getAndIncrement方法,它里面实现了基于CAS的自旋。

现在已经了解乐观锁及CAS相关机制,乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:

①  观锁只能保证一个共享变量的原子操作。如上例子,自旋过程中只能保证value变量的原子性,这时如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。

②  长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。

③  ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提高并发性能的主要手段,而且jdk中的并发包也大量使用基于CAS的乐观锁。

免责声明:文章转载自《(转)乐观的并发策略——基于CAS的自旋》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Jmeter 重要测试指标释义Haproxy安装配置及日志输出问题下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

SpringBoot:异步开发之异步调用

前言除了异步请求,一般上我们用的比较多的应该是异步调用。通常在开发过程中,会遇到一个方法是和实际业务无关的,没有紧密性的。比如记录日志信息等业务。这个时候正常就是启一个新线程去做一些业务处理,让主线程异步的执行其他业务。所以,本章节重点说下在SpringBoot中如何进行异步调用及其相关知识和注意点。 何为异步调用 说异步调用前,我们说说它对应的同步调用。...

C++线程中的几种锁

线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,锁的功能越强大,性能就会越低。 1、互斥锁 互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错...

Java虚拟机:十八、Java对象大小、对象内存布局及锁状态变化

一个对象占多少字节? 关于对象的大小,对于C/C++来说,都是有sizeof函数可以直接获取的,但是Java似乎没有这样的方法。不过还好,在JDK1.5之后引入了Instrumentation类,这个类提供了计算对象内存占用量的方法。至于具体Instrumentation类怎么用就不说了,可以参看这篇文章如何精确地测量java对象的大小。 不过有一点不同的...

线程中断总结

1、线程中断 每个线程都有一个与之相关联的 Boolean 属性,用于表示线程的中断状态(interrupted status)。中断状态初始时为 false;当另一个线程通过调用Thread.interrupt()中断一个线程时,会出现以下两种情况之一。如果那个线程在执行一个低级可中断阻塞方法,例如Thread.sleep()、Thread.join()...

java虚拟机启动参数分类详解

java启动参数共分为三类;其一是标准参数(-),所有的JVM实现都必须实现这些参数的功能,而且向后兼容;其二是非标准参数(-X),默认jvm实现这些参数的功能,但是并不保证所有jvm实现都满足,且不保证向后兼容;其三是非Stable参数(-XX),此类参数各个jvm实现会有所不同,将来可能会随时取消,需要慎重使用; 标准参数(-) verbose ...

华为内部面试题库(10)

1. 对于linux内核信号量,说法正确的是(多选):(参考:Linux内核设计与实现,第二版,第9章,9.4小节) A. 如果获取一个被占用的信号量,任务会睡眠,等待信号量释放之后,该任务才能重新获得调度 B. 信号量可以允许任意数量的锁持有者 C. 信号量保护的代码可以被抢占 D. 信号量的实现也是与体系架构相关的 答案:A,B,C,D 试题解析:信号...