文章22 | 阅读 8034 | 点赞0
乐观锁是一种思想,在进行读写操作的时候都不会加锁,但在进行写操作的时候会判断数据是否被修改过,这一点可以使用版本号机制或 CAS 算法实现。乐观锁适用于读多写少的场景,可以提高吞吐量,数据库的 write_condition 机制(通过版本号机制实现)和 JUC 包下的原子变量类(通过 CAS 算法实现)都是基于乐观锁思想实现的。
版本号机制一般都是在数据库中加一个 version 字段作为判断依据,举例如下:
假设数据库中有一个余额表,balance 字段表示当前余额,值为 10000;version 字段表示当前版本,值为 1。
这样就避免了线程 B 提交的数据覆盖线程 A 提交的数据,从而导致数据丢失的情况的发生了。
CAS 全拼是 Compare And Swap(比较与替换),CAS 算法涉及到三个基本的操作数:
当且仅当 V 和 A 的值相等时,CAS 通过原子的方式(比较和替换两个操作在 CAS 算法中是原子的)将新值 B 赋值给内存值 V,如果 V 和 A 的值不相等的话,一般情况下会进行自旋操作,即重新获取内存值,然后继续之前的流程,直到操作成功。
只看上面一段话可能有点抽象,下面我将通过图示法来进行解析。
我们来分步写一下 CAS 算法的整个操作流程。
1.ABA 问题
我们先思考一个问题,如果一个变量 V 在初次读取时值为 A,在准备赋值的时候检测到 V 的值还是 A 就一定能说明 V 没有被其他线程修改过吗?当然不,因为有可能 V 的值先别修改为 B,然后又被修改为 A,那么 CAS 算法就会认为 V 的值没有被修改过。这个问题就被称为 ABA 问题。
JDK 1.5 以后的 AtomicStampedReference 类提供了解决 ABA 问题的办法,其中的 compareAndSet 方法会首先检查当前引用是否等于预期引用,然后再检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2.长时间循环执行造成 CPU 开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
3.只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在同一个对象里来进行 CAS 操作。当然也可以使用 Synchronized。
悲观锁是一种思想,每次读或写操作都会加锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程,适用于写多读少的场景。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁。Java 中 synchronized 和 ReentrantLock 等独占锁都是悲观锁思想的实现。
简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
synchronized
同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。补充: Java并发编程这个领域中synchronized
关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized
的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
参考文章:CAS算法的理解及应用,面试必备之乐观锁与悲观锁
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/weixin_41685207/article/details/109264929
内容来源于网络,如有侵权,请联系作者删除!