并发编程的悲观锁和乐观锁 - Go语言中文社区

并发编程的悲观锁和乐观锁


悲观锁和乐观锁 是并发情境下的两种设计思想, 它们的主要区别在于:

悲观锁则认为肯定会发生并发问题, 要么我等着, 要么就让别人等;
乐观锁认为当前发生并发的可能性不大, 我先试试, 不行的话再说.

一般只有在高并发下使用悲观锁才比较合适, 并发不是很严重的情况下使用乐观锁会有更高的效率.


Java里的悲观锁和乐观锁

在Java中, Synchronized 和 Lock 是悲观锁, 典型的乐观锁是 Unsafe.CAS 方法 ( 冲突检测和数据更新, Compare and Swap ).

CAS 操作中传入三个参数: 需要读写的内存位置(V), 进行比较的预期原值(A)和拟写入的新值(B). 如果内存位置V的值与预期原值A相匹配, 那么处理器会自动将该位置值更新为新值B, 否则处理器不做任何操作.
如果CAS方法失败会返回false, 用户可以通过此返回值判断成功与否, 可以进行自旋操作直到执行成功.

Synchronized与CAS的使用情景:   

  1. 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
  2. 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

ps. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。


Java源码体现

java.util.concurrent 提供了在并发编程中很常用的实用工具类, 这个包里面有以下几类

其中 atomic 包下的各个类均是使用 Unsafe.CAS 方法实现的原子操作类, ConcurrentMap等也是使用的Unsafe.CAS.

关于 Atomic 类, 在我的另一篇博客里有较为详细的说明
从源码看Android常用的数据结构 ( 一 , 总述 )


乐观锁 CAS 的缺陷以及修正办法

缺点主要有以下三个

1. ABA问题 :

如果变量由A变为了B又变回了A, CAS可能认为变量没有被修改过, 然后直接更新, 这可能会导致问题. 例如以下情况:

对于 A(next=B) -- B(next=null) 这样一个链表, 两个线程同时操作, 线程1执行 CAS(栈顶内存, A, B) 希望把栈顶由A替换成B, 而线程2优先执行完毕了移除AB/重新添加元素等操作, 另链表变为了 A(next=C) -- C(next=D) -- D(next=null), 然后线程1的CAS开始执行, 检测发现顶部仍然是A ( 虽然next已经改变了 ), 然后CAS执行成功, 顶部元素更改为了 B(next=null), 这样 C(next=D) -- D(next=null) 就无缘无故的消失了…

如果规避ABA问题?
在Java1.5之后的JDK的atomic包里提供了一个类AtomicStampedReference. 这个类的 compareAndSet 方法首先检查当前引用是否等于预期引用/当前标志是否等于预期标志, 如果全部相等, 则以原子方式将该引用和该标志的值设置为给定的更新值。

/**
* expectedReference 预期引用
* newReference      更新后的引用
* expectedStamp     预期标志
* newStamp          更新后的标志
*/
boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)

// 这里的标志stamp就相当于版本号, 双重检测保证数据是否是更新后的
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);

2. 自旋循环时间长时CPU开销大 :

自旋CAS( 不成功, 就一直循环执行, 直到成功 ) 如果长时间不成功, 会给CPU带来非常大的执行开销.
如果JVM能支持处理器提供的pause指令那么效率会有一定的提升 ( 那到底能不能呢? 搜了下也没找到答案… )
pause指令有两个作用:

  1. 它可以延迟流水线执行指令(de-pipeline), 使CPU不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本, 在一些处理器上延迟时间是零;
  2. 它可以避免在退出循环的时候因内存顺序冲突 (memory order violation) 而引起CPU流水线被清空(CPU pipeline flush), 从而提高CPU的执行效率.

3. 只能保证一个共享变量的原子操作 :

当对一个共享变量执行操作时, 我们可以使用循环CAS的方式来保证原子操作, 但是对多个共享变量操作时, 循环CAS就无法保证操作的原子性了, 这个时候就可以用锁.
或者使用 AtomicReference 把多个变量放在一个对象里来进行CAS操作.


如有不对, 欢迎批评.

参考文章: https://www.cnblogs.com/qjjazry/p/6581568.html

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/j550341130/article/details/80930393
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 12:02:47
  • 阅读 ( 1414 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢