社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
悲观锁和乐观锁 是并发情境下的两种设计思想, 它们的主要区别在于:
悲观锁则认为肯定会发生并发问题, 要么我等着, 要么就让别人等;
乐观锁认为当前发生并发的可能性不大, 我先试试, 不行的话再说.
一般只有在高并发下使用悲观锁才比较合适, 并发不是很严重的情况下使用乐观锁会有更高的效率.
在Java中, Synchronized 和 Lock 是悲观锁, 典型的乐观锁是 Unsafe.CAS 方法 ( 冲突检测和数据更新, Compare and Swap ).
CAS 操作中传入三个参数: 需要读写的内存位置(V), 进行比较的预期原值(A)和拟写入的新值(B). 如果内存位置V的值与预期原值A相匹配, 那么处理器会自动将该位置值更新为新值B, 否则处理器不做任何操作.
如果CAS方法失败会返回false, 用户可以通过此返回值判断成功与否, 可以进行自旋操作直到执行成功.
Synchronized与CAS的使用情景:
ps. synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
java.util.concurrent 提供了在并发编程中很常用的实用工具类, 这个包里面有以下几类
其中 atomic 包下的各个类均是使用 Unsafe.CAS 方法实现的原子操作类, ConcurrentMap等也是使用的Unsafe.CAS.
关于 Atomic 类, 在我的另一篇博客里有较为详细的说明
从源码看Android常用的数据结构 ( 一 , 总述 )
缺点主要有以下三个
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指令有两个作用:
3. 只能保证一个共享变量的原子操作 :
当对一个共享变量执行操作时, 我们可以使用循环CAS的方式来保证原子操作, 但是对多个共享变量操作时, 循环CAS就无法保证操作的原子性了, 这个时候就可以用锁.
或者使用 AtomicReference 把多个变量放在一个对象里来进行CAS操作.
如有不对, 欢迎批评.
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!