浅谈一下Java中的悲观锁和乐观锁
作者:索码理 发布时间:2023-08-12 05:54:27
悲观锁和乐观锁是面试高频问题之一,本文将对悲观锁和乐观锁简单的进行一个介绍。
悲观锁(Pessimistic Locking)
悲观锁在并发环境中认为数据随时会被其他线程修改,因此每次在访问数据时都会加锁,直到操作完成后才释放锁。悲观锁适用于写操作多、竞争激烈的场景,比如多个线程同时对同一数据进行修改或删除操作的情况。悲观锁可以保证数据的一致性,避免脏读、幻读等问题的发生
悲观锁就像一个大保安,总是认为有坏人想要偷走共享资源,于是它把资源护得紧紧的,不让任何人接近,同时还会排队等待资源,想要使用就得先获取锁,这样虽然安全可靠,但是也会导致效率低下,因为别的线程必须等待锁的释放才能继续执行。
Java中常用的悲观锁是synchronized关键字和ReentrantLock类。
使用synchronized关键字实现悲观锁的代码如下:
synchronized (lock) {
//访问共享资源的代码块
}
使用ReentrantLock实现悲观锁的代码如下:
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
//访问共享资源的代码块
} finally {
lock.unlock();
}
悲观锁存的问题:
效率低:悲观锁需要获取锁才能进行操作,当有多个线程需要访问同一份数据时,每个线程都需要先获取锁,然后再进行操作,如果锁竞争激烈,就会导致线程等待锁的释放,浪费了大量的时间。
容易引起死锁:悲观锁在获取锁的过程中,如果获取不到就会一直等待,如果不同的线程都在等待对方释放锁,就会导致死锁的情况出现。
可能会引起线程阻塞:当某个线程获取到锁时,其他线程需要等待,如果等待的时间过长,就会导致线程阻塞,影响应用的性能。
乐观锁
乐观锁在并发环境中认为数据一般情况下不会被其他线程修改,因此在访问数据时不加锁,而是在更新数据时进行检查。如果检查到数据被其他线程修改,则放弃当前操作,重新尝试更新。
乐观锁适用于读操作多、写操作少的场景,比如多个线程同时对同一数据进行读取操作的情况。乐观锁可以减少锁的竞争,提高系统的并发性能。
乐观锁就像一个乐天派,总是认为没有坏人想要偷走共享资源,于是它就不怎么防范,直接对资源进行操作,如果没有其他线程对资源进行修改,操作就会成功,否则就会进行重试,这样虽然效率高,但是如果多个线程同时进行修改,就会导致竞争和冲突,需要进行额外的处理。
Java中常用的乐观锁是基于CAS(Compare and Swap,比较和交换)算法实现的。
CAS操作包括三个操作数:内存地址V、旧的预期值A和新的值B。CAS操作首先读取内存地址V中的值,如果该值等于旧的预期值A,那么将内存地址V中的值更新为新的值B;
否则,不进行任何操作。在更新过程中,如果有其他线程同时对该共享资源进行了修改,那么CAS操作会失败,此时需要重试更新操作。
下面是一段基于CAS算法实现的乐观锁代码:
// 假设共享资源为变量value,初始值为1
AtomicInteger value = new AtomicInteger(1);
// 假设旧的预期值为1,新的值为2
int expect = 1;
int update = 2;
// 使用CAS操作更新共享资源的值
while (true) {
// 读取共享资源的当前值
int current = value.get();
// 如果当前值等于旧的预期值,使用CAS操作将新的值更新到共享资源中
if (current == expect) {
if (value.compareAndSet(expect, update)) {
// 更新成功,退出循环
break;
} else {
// 更新失败,可能是因为其他线程修改了共享资源的值,重试更新操作
continue;
}
} else {
// 当前值不等于旧的预期值,说明共享资源的值已经被其他线程修改,重试更新操作
continue;
}
}
在这段代码中,内存地址V对应的是AtomicInteger对象value,旧的预期值A对应的是变量expect,新的值B对应的是变量update。使用AtomicInteger对象可以保证CAS操作的原子性,即只有一个线程能够成功更新共享资源的值。使用compareAndSet
方法可以判断共享资源的值是否等于旧的预期值,并尝试将新的值更新到共享资源中。如果更新成功,就退出循环;否则,说明共享资源的值已经被其他线程修改,需要重试更新操作。
在实际应用中,乐观锁的实现通常比这个简单实现要复杂。例如,在对数据库中的数据进行更新时,需要在更新操作中同时更新版本号和其他字段的值,并且需要处理更新失败和重试的情况。
乐观锁存在的问题
CAS虽然很⾼效的解决原⼦操作,但是CAS仍然存在三⼤问题:ABA问题,自旋时间过长和只能保证单个变量的原子性。
ABA问题:CAS算法在比较和替换时只考虑了值是否相等,而没有考虑到值的版本信息。如果一个值在操作过程中被修改了两次,从原值变成新值再变回原值,此时CAS会认为值没有发生变化,从而出现操作的错误。为了解决ABA问题,可以在共享资源中增加版本号,每次修改操作都将版本号加1,从而保证每次更新操作的唯一性。在更新数据时先读取当前版本号,如果与自己持有的版本号相同,则可以更新数据,否则更新失败。版本号算法可以避免ABA问题,但需要维护版本号,增加了代码复杂度和内存开销。
自旋时间过长:由于CAS算法在失败时会一直自旋,等待共享变量可用,如果共享变量一直不可用,就会出现自旋时间过长的问题,浪费CPU资源。
只能保证单个变量的原子性:CAS算法只能保证单个变量的原子性,如果需要多个变量的原子操作,就需要使用锁等其他方式进行保护。
悲观锁和乐观锁的对比
悲观锁 | 乐观锁 | |
性能 | 低 | 高 |
数据一致性 | 高 | 低 |
实现复杂度 | 简单 | 复杂 |
加锁方式 | 基于锁机制 | 基于版本号机制 |
应用场景 | 读少写多 | 读多写少 |
存在的问题 | 效率低、容易引起死锁、可能会引起线程阻塞 | ABA问题、自旋时间过长、只能保证单个变量的原子性 |
来源:https://blog.csdn.net/qq_39654841/article/details/129238235


猜你喜欢
- 用了多年的Visual Studio,今天才发现这个编码技巧,真是惭愧,分享出来,算是抛砖引玉吧!开发环境: vs2010+C#1、代码重构
- AuditEnum.cs:public enum AuditEnum{ Holding=0, Audit
- 最近公司项目中有一个类似滴滴出行填写验证码的弹框,下面是我撸出来的效果: 中间的那个输入密码的6个框框其实就是用shape画的背景
- 前言:在多线程编程中,wait 方法是让当前线程进入休眠状态,直到另一个线程调用了 notify 或 notifyAll 方法之后,才能继续
- Android系统对所有的危险权限进行了分组,称为 权限组 。属于同一组的危险权限将自动合并授予,用户授予应用某个权限组的权限,则应用将获得
- 一、Maven聚合开发_继承关系 Maven中
- 1. 前言Guava是一个由Google开发的Java核心库,它提供了很多有用的方法和实用工具类,可以帮助开发人员提高代码质量和开发效率。在
- 概述新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效
- 最近设计要求要一个圆形进度条渐变的需求:1.画圆形进度条2.解决渐变最终实现效果代码package com.view;import andr
- 如下所示:package com.unionx.wanxue; import java.util.Map; import java.util
- 前一段时间粗略看了一下《深入Java虚拟机 第二版》,可能是因为工作才一年的原因吧,看着十分的吃力。毕竟如果具体到细节的话,Java虚拟机涉
- 前言最近看插件库上少有的取色器大都是圆形的或者奇奇怪的的亚子,所以今天做两个矩形的颜色取色器提示:以下是本篇文章正文内容,下面案例可供参考一
- 简介本文介绍微服务架构中如何实现单点登录功能创建三个服务:操作redis集群的服务,用于多个服务之间共享数据统一认证中心服务,用于整个系统的
- 队列和堆栈都是约束版的链表,就像在超市购物,队列是先进先出的数据结构。接着上一篇,派生于链表类List,来模拟一个队列。namespace
- Java 执行 JS 脚本工具用途:为了便于系统扩展,提供了 JS 脚本的功能,可以通过在系统中执行脚本来获得更复杂的功能。例如:系统提供了
- 上篇并发编程之Java内存模型volatile的内存语义介绍了volatile的内存语义,本文讲述的是final的内存语义,相比之下,fin
- 1.如果执行了try块没有异常,则继续运行finally块中的语句,即使try块通过return,break,或者continue于最后的语
- 一、字符流的出现中文在GBK中占有两个字节,在utf-8中占有三个字节(即需要三个字节才能组成一个中文字),字节流读取中文时由于编码集的不同
- 一.BASIC认证概述在HTTP协议进行通信的过程中,HTTP协议定义了基本认证过程以允许HTTP服务器对WEB浏览器进行用户身份证的方法,
- class MyThreadScopeData { // 单例 &nbs