天天看点

关于乐观锁和悲观锁

一、概念

悲观锁:总是假设最坏的情况,认为竞争总是存在,每次拿数据的时候都认为会被修改,因此每次都会先上锁。其他线程阻塞等待释放锁。

乐观锁:总是假设最好的情况,认为竞争总是不存在,每次拿数据的时候都认为不会被修改,因此不会先上锁,在最后更新的时候比较数据有无更新,可通过版本号或CAS实现。

二、两种锁的使用场景

悲观锁:用于写比较多的情况,避免了乐观锁不断重试从而降低性能

乐观锁:用于读比较多的情况,避免了不必要的加锁的开销

三、乐观锁的实现方式

版本号机制:

一般通过在数据库增加version列实现。

CAS(compare and swap):

需要三个操作数

需要读写的内存值 V

进行比较的值 A

拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

四、乐观锁的缺点

1.ABA问题

2.自旋时间CPU开销大

3.JUC包下的原子类只能包含一个变量的原子操作,但是1.5之后的AtomicReference,能够保证引用对象的原子性。

五、CAS与synchronized的使用情景

CAS属于乐观锁,适用于写比较少的情况,冲突较少

synchronized属于悲观锁,适用于冲突写比较多的情况,冲突较多

竞争较少的场景:synchronized会阻塞和唤醒线程并在用户态和内核态切换浪费消耗cpu资源。CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

竞争严重的场景:CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充:

synchronized被称为“重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

2.阿里巴巴java开发手册:如果线程访问冲突小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不小于3次。

继续阅读