Java并发系列三-乐观锁

乐观与悲观:
假设现在有多个线程想要操作同一个资源对象,很多人的第一反应就是使用互斥锁。但互斥锁的同步方式是悲观的,什么是悲观呢?简单来说,就是操作系统将会悲观的认为,如果不严格同步线程调用,那么一定会产生异常,所以互斥锁将会锁定资源,只供一个线程调用,而阻塞其他线程,让其他线程等待,因此,这种同步机制也叫做悲观锁。

但悲观锁不是在所用情况下都适用,比如在一些情况下,同步代码块执行的耗时远远小于线程切换的耗时,这样就很不划算。程序员们可能更加希望一些场景下,能够在用户态中对线程的切换进行管理,这样效率更高。所以,我们不想让操作系统那么悲观,每次都使用同步原语对共享资源进行锁定,而是希望让线程反复”乐观“地去尝试获取共享资源,如果发现空闲,那么使用,如果被占用,那么继续乐观的重试。


CAS
理想很丰满,那么具体该如何实现呢?相比一定有无数前人思考过这个问题,在他们的努力下,诞生了一种非常经典的巧妙的算法叫做CAS(Compare And Swap)。可以简单翻译为:比较然后交换。很多人都听说过CAS,但是对于它究竟是如何工作的?需要哪些外部支持?如何应用到业务中?可能并不是很了解,下面我就通过一个通俗的例子来进行介绍。

我们现在假设有一间更衣室,房间门上挂着一块牌子,正面是0,反面是1,这块牌子代表房间是否被占用的状态。当显示0的时候,房间为空,谁都可以进入,当显示1时,则代表有人正在使用。在上面这个比喻里,房间就是共享资源,号码牌就是一把乐观锁,人就是线程。

假设此时A和B这两条线程都看到了牌子上显示的是0,于是争抢着去使用房间。但是A线程抢险获得了时间片,他第一个冲进房间并将这块牌子的状态改为1,此时B线程才冲过来,但是发现牌子上的状态已经被改为1,不过B线程没有放弃,不断回来看看牌子变为0了没。

这样,你应该就能够很容易地理解CAS,当共享资源的状态为0的一瞬间,A、B线程读到了。此时,这两条线程认为共享资源当前空闲未被占用,于是他们各自将会生成两个值。
1.old value 代表之前读到的资源对象的状态值。
2.new value 代表想要将资源对象的状态值更新后的值。
这里对AB线程来说,old value都是0,new value都是1.

此时AB线程争抢着去修改资源对象的状态值,然就占用它。假设A线程运气比较好,率先获得时间片时,他将old value与资源对象的状态值进行compare,发现一致,于是将牌子上的值swap为new value.而线程B没有那么幸运,它落后了一步,此时资源对象的状态值已经被A线程修改成了1,所以B线程在compare的时候,发现和自己预期的old value不一致,所以放弃swap操作。
但在实际应用中,我们不会让B线程就这么放弃,通常会使其自旋,自旋就是使其不断重试cas操作,通常会配置自旋次数来防止死循环。

因为看上去这个CAS函数本身没有进行任何同步措施,似乎还是存在线程不安全的问题。比如A线程看到牌子的状态是0,伸手去翻的一瞬间,很有可能B线程突然抢到时间片,将牌子翻成了1,但是线程A不知情,也将牌子翻到了1,这就出现了线程安全问题,AB线程同时获得了资源,好比两个人进入了更衣室,非常尴尬。

这么看来,一个亟待解决的问题是,”比较数值是否一致并且修改数值“的这个动作,必须要么成功要么失败,不能存在中间状态,换句话说,CAS操作必须是原子性的。只有基于这个真理,我们前面的所有设想才能成立。

那么,如何实现CAS的原子性呢?所幸的是,各种不同架构的CPU都提供了指令级的CAS原子操作,比如在x86架构下,通过cmpxchg指令支持CAS,在ARM下,通过LL/SC来实现CAS.也就是说,既然CPU已经原生地支持了CAS,那么上层进行调用即可。现在,除了通过操作系统的同步原语(比如mutex)来有锁地实现线程同步(悲观),通过CAS的方式我们能实现另一种无锁的同步机制(乐观)。

万丈高楼平地起,在计算机世界中更是如此,底层开放一点新特性,上层就获得了施展手脚的舞台。有了CPU指令级对CAS的支持,以此为基础,就能促进上层产生多种多样无锁编程的思路,以及诞生各种各样好用的库和工具。

这些通过CAS来实现同步的工具,由于不会锁定资源,而且当线程需要修改共享资源对象时,总是会乐观的认为对象状态值没有被其他线程修改过,自己主动尝试去Compare And Set状态值,相较于上文提到的”悲观锁“,这种同步机制被称作”乐观锁“

Java中的乐观锁编程
假设有一个简单的需求,你需要使用三条线程,将一个值,从0累加到1000,你该怎么做
首先我写一种错误的写法,不使用任何同步操作,那么一定会出现线程安全问题。


最常规的,我们可以通过悲观锁来同步线程,但是这并不是我们今天的重点,今天我们的重点是乐观锁。


如何使用乐观锁实现呢?非常简单。我们要善用轮子,写过Java的同学应该都知道AtomicInteger这个类,它的底层通过CAS来实现了同步的计数器。我们可以将代码改成这样


AtomicInteger这个类的内容并不多,主要的成员变量就是一个Unsafe类型的实例和一个Long类型的offset,这边注释也开门见山,告诉我们使用Unsafe的CAS操作来对值进行更新。我们看incrementAndGet方法,可以看到直接调用了Unsafe对象的getAndAddInt方法,进一步点进去,可以看到确实就是调用了Unsafe的compareAndSwapInt方法(CAS)。这里出现了一个循环,实际上就是我之前提及的自旋。
有的同学会问,假如这边CAS操作一直失败,那么会不会一直死循环下去?问得好,自旋的次数可以通过启动参数来配置,如果你不配置的话,默认是10,所以不会出现死循环。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值