【八股】乐观锁与悲观锁

news/2024/11/16 19:36:49/

如果将悲观锁(Pessimistic Lock)乐观锁(PessimisticLock 或 OptimisticLock) 对应到现实生活中来。悲观锁有点像是一位比较悲观(也可以说是未雨绸缪)的人,总是会假设最坏的情况,避免出现问题。乐观锁有点像是一位比较乐观的人,总是会假设最好的情况,在要出现问题之前快速解决问题。

在程序世界中,乐观锁和悲观锁的最终目的都是为了保证线程安全,避免在并发场景下的资源竞争问题。但是,相比于乐观锁,悲观锁对性能的影响更大!

什么是悲观锁?

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

public void performSynchronisedTask() {synchronized (this) {// 需要同步的操作}
}private Lock lock = new ReentrantLock();
lock.lock();
try {// 需要同步的操作
} finally {lock.unlock();
}

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

什么是乐观锁?

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

像 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好
// 代价就是会消耗更多的内存空间(空间换时间)
LongAdder longAdder = new LongAdder();
// 自增
longAdder.increment();
// 获取结果
longAdder.sum();

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试(悲观锁的开销是固定的),这样同样会非常影响性能,导致 CPU 飙升。

不过,大量失败重试的问题也是可以解决的,像我们前面提到的 LongAdder以空间换时间的方式就解决了这个问题。

理论上来说:

  • 悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。
  • 乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。

如何实现乐观锁?

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。

版本号机制

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

**举一个简单的例子 :**假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A操作的过程中,操作员 B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员A 完成了修改工作,将数据版本号( version=1 ),连同帐户扣除后余额( balance=$50
    ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
    操作员 B完成了操作,也将版本号( version=1 )试图向数据库提交数据( balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。

CAS 算法

CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。

CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。

原子操作 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。

CAS 涉及到三个操作数:

  • V :要更新的变量值(Var)
  • E :预期值(Expected)
  • N :拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

举一个简单的例子 :线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。

  1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。
  2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。

sun.misc包下的Unsafe类提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作

/***  CAS* @param o         包含要修改field的对象* @param offset    对象中某field的偏移量* @param expected  期望值* @param update    更新值* @return          true | false*/
public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object update);public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

乐观锁存在哪些问题?

ABA 问题是乐观锁最常见的问题。

ABA 问题
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V   expectedReference,V   newReference,int expectedStamp,int newStamp) {Pair<V> current = pair;returnexpectedReference == current.reference &&expectedStamp == current.stamp &&((newReference == current.reference &&newStamp == current.stamp) ||casPair(current, Pair.of(newReference, newStamp)));
}

循环时间长开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用:

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

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

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

总结

  • 高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。不过,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
  • 乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。
  • CAS 的全称是 Compare And Swap(比较与交换) ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。
  • 乐观锁的问题:ABA 问题、循环时间长开销大、只能保证一个共享变量的原子操作。

http://www.ppmy.cn/news/508557.html

相关文章

显示黑科技点亮魔都 DISPLAY CHINA六月开展

“2019国际新型显示技术展” (DISPLAY CHINA 2019)致力于打造一场新型显示与触摸屏行业盛会&#xff0c;从技术、产品、市场、发展等多角度出发&#xff0c;结合当前折叠屏、5G等热点&#xff0c;为面临市场挑战的行业同仁提供一个全面专业的贸易平台&#xff0c;共同解决全新时…

中国新型显示产业竞争格局与发展规模预测报告2022版

中国新型显示产业竞争格局与发展规模预测报告2022版 HS--HS--HS--HS--HS--HS--HS--HS--HS--HS--HS--HS-- 【修订日期】&#xff1a;2021年11月 【搜索鸿晟信合研究院查看官网更多内容&#xff01;】 第一章 新型显示相关概述 1.1 新型显示技术简介 1.1.1 液晶显示&#x…

京东方、天马、惠科、华佳彩、JDI等面板企业重磅亮相!多款黑科技展品抢先看!...

近年来&#xff0c;显示应用不仅仅局限于手机、电视等消费类电子产品&#xff0c;同时也向高端车载、笔记本电脑、商超、可穿戴设备等领域渗透。面板企业布局逐渐扩大&#xff0c;各种新型显示产品与技术层出不穷。 2019国际新型显示技术展 (DISPLAY CHINA 2019)将于6月26-28日…

帆软助力惠科集团产值提升1%以上,带来上亿元数据价值

日前&#xff0c;国际知名咨询机构IDC公布了“2021 IDC中国未来企业大奖”优秀案例获奖名单。惠科集团携手帆软打造的“惠科集团数据中心项目”在激烈的竞争中脱颖而出&#xff0c;荣膺“未来智能领军者”大奖。 IDC“未来企业大奖”原为“数字化转型大奖”&#xff0c;自2017年…

显示器屏幕有时黑几秒又恢复相关解决方法

记录-屏幕时不时黑几秒又恢复 1. 屏幕黑几秒恢复后屏幕出现相关提示&#xff1a;显卡驱动失去工作等等提示并可以看到显卡驱动的版本号。 解决方法&#xff1a; 更换其他版本的显卡驱动。英伟达官方显卡驱动下载&#xff1a;https://www.geforce.cn/drivers 可以上网搜索该驱…

全球LCD面板产能加速向我国转移,车载显示屏方兴未艾

导 读&#xff1a;全球 LCD 产业转移和汽车智能化共同推动我国 LCD 面板行业快速发展&#xff0c;这将带来背光模组需求的持续增加&#xff0c;我国背光模组行业具有较大的发展潜力。 LCD 面板 背光模组由背光光源、导光板、光学膜片和塑胶框组成&#xff0c;是 LCD 显示面板的…

小米MIUI11.0.5无法安装chrome,

问题描述:小米MIUI11.0.5无法安装chrome,会在安装的过程中出现错误提示:"...permission denied" 解决方案: 进入"我的设备"-全部参数-一直点击MIUI版本,直接出现"开发者模式",关闭开发者选项中的MIUI优化并重启&#xff01;

mysql从旧电脑转移到新电脑

1、从旧电脑找到mysql安装目录复制整个文件夹到新电脑 2、配置新电脑mysql环境变量&#xff0c;在系统变量中的path中新增mysql bin目录的路径 3、用管理员模式打开cmd窗口执行以下命令 mysqld initialize mysqld install 最后 net start mysql