当前位置: 首页 > news >正文

Java 悲观锁 乐观锁

锁可以从不同的角都分类。其中乐观锁和悲观锁是一种分类方式

一、悲观锁、乐观锁定义

悲观锁就是我们常说到的锁。对于悲观锁来说,他总是认为每次访问共享资源时会发生冲突,所以必须每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突, 乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。

由于无锁操作中没有锁的存在,因此不肯能出现死锁的情况,也就是说乐观锁天生免疫死锁。

乐观锁多用于“读多写少”的环境,避免频繁加锁影响性能;而悲观锁锁用于“写多读少”的环境。避免频繁失败和重试影响性能。

二、实现方式

悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁。synchronized关键字和Lock的实现类都是悲观锁。

乐观锁的实现方式主要有两种:CAS机制和版本号机制。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

1、CAS(Compare And Swap)
CAS操作包括了3个操作数:
  • 需要读写的内存位置(V)
  • 进行比较的预期值(A)
  • 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

下面以Java中的自增操作(i++)为例,看一下悲观锁和CAS分别是如何保证线程安全的。我们知道,在Java中自增操作不是原子操作,它实际上包含三个独立的操作:(1)读取i值;(2)加1;(3)将新值写回i。

因此,如果并发执行自增操作,可能导致计算结果的不准确。在下面的代码示例中:value1没有进行任何线程安全方面的保护,value2使用了乐观锁(CAS),value3使用了悲观锁(synchronized)。运行程序,使用1000个线程同时对value1、value2和value3进行自增操作,可以发现:value2和value3的值总是等于1000,而value1的值常常小于1000。
 

public class suo {//value1:线程不安全private static int value1 = 0;//value2:使用乐观锁private static AtomicInteger value2 = new AtomicInteger(0);//value3:使用悲观锁private static int value3 = 0;private static synchronized void increaseValue3(){value3++;}public static void main(String[] args) throws Exception {//开启1000个线程,并执行自增操作for(int i = 0; i < 1000; ++i){new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}value1++;value2.getAndIncrement();increaseValue3();}}).start();}//打印结果Thread.sleep(1000);System.out.println("线程不安全:" + value1);System.out.println("乐观锁(AtomicInteger):" + value2);System.out.println("悲观锁(synchronized):" + value3);}
}

输出

线程不安全:991
乐观锁(AtomicInteger):1000
悲观锁(synchronized):1000

首先来介绍AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子类,利用CPU提供的CAS操作来保证原子性;除了AtomicInteger外,还有AtomicBoolean、AtomicLong、AtomicReference等众多原子类。
java是无法实现对底层内存的操作的,C++可以,java使用Unsafe类实现。
 

public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// setup to use Unsafe.compareAndSwapInt for updatesprivate static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}private volatile int value;
  • unsafe: 获取并操作内存的数据。
  • valueOffset: 存储value在AtomicInteger中的偏移量。
  • value: 存储AtomicInteger的int值,该属性需要借助volatile关键字保证其在线程间是可见的。

我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()。但是由于JDK本身只有Unsafe.class,只通过class文件中的参数名,并不能很好的了解方法的作用,所以我们通过OpenJDK 8 来查看Unsafe的源码:

// ------------------------- JDK 8 -------------------------
// AtomicInteger 自增方法
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));return v;
}

根据OpenJDK 8的源码我们可以看出,getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

其他源码:
 

 public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}
public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • getAndIncrement()实现的自增操作是自旋CAS操作:在循环中进行compareAndSet,如果执行成功则退出,否则一直执行。
  • 其中compareAndSet是CAS操作的核心,它是利用Unsafe对象实现的。
  • Unsafe又是何许人也呢?Unsafe是用来帮助Java访问操作系统底层资源的类(如可以分配内存、释放内存),通过Unsafe,Java具有了底层操作能力,可以提升运行效率;强大的底层资源操作能力也带来了安全隐患(类的名字Unsafe也在提醒我们这一点),因此正常情况下用户无法使用。AtomicInteger在这里使用了Unsafe提供的CAS功能。
  • valueOffset可以理解为value在内存中的偏移量,对应了CAS三个操作数(V/A/B)中的V;偏移量的获得也是通过Unsafe实现的。
  • value域的volatile修饰符:Java并发编程要保证线程安全,需要保证原子性、可视性和有序性;CAS操作可以保证原子性,而volatile可以保证可视性和一定程度的有序性;在AtomicInteger中,volatile和CAS一起保证了线程安全性。关于volatile作用原理的说明涉及到Java内存模型(JMM),这里不详细展开。

2、版本号机制

除了CAS,版本号机制也可以用来实现乐观锁。版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。

需要注意的是,这里使用了版本号作为判断数据变化的标记,实际上可以根据实际情况选用其他能够标记数据版本的字段,如时间戳等。

三、优缺点和适用场景

1、功能限制

与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。

例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。

2、竞争激烈程度

如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:

当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

四、乐观锁加锁吗?

(1)乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了;AtomicInteger便是一个例子。

(2)有时乐观锁可能与加锁操作合作。

五、CAS有哪些缺点

1、ABA问题

假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

(1)线程1读取内存中数据为A;

(2)线程2将该数据修改为B;

(3)线程2将该数据修改为A;

(4)线程1对数据进行CAS操作

在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。

在AtomicInteger的例子中,ABA似乎没有什么危害。但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。

对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。Java中的AtomicStampedReference类便是使用版本号来解决ABA问题的。这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

2、循环时间长开销大

CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

3、功能限制

只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

相关文章:

Java 悲观锁 乐观锁

锁可以从不同的角都分类。其中乐观锁和悲观锁是一种分类方式 一、悲观锁、乐观锁定义 悲观锁就是我们常说到的锁。对于悲观锁来说&#xff0c;他总是认为每次访问共享资源时会发生冲突&#xff0c;所以必须每次数据操作加上锁&#xff0c;以保证临界区的程序同一时间只能有一个…...

优惠券秒杀(二)

库存超卖问题分析 库存超卖问题其本质就是多个线程操作共享数据产生的线程安全问题&#xff0c;即当一个线程在执行操作共享数据的多条代码的过程中&#xff0c;其他线程也参与了进来&#xff0c;导致了线程安全问题的产生。例如&#xff1a;线程1发送请求&#xff0c;查询库存…...

selenium的java方式打开IE浏览器

1.下载软件Selenium Driver 官方下载地址&#xff1a; ​ https://www.selenium.dev/downloads/解压selenium-java-3.141.59.zip文件到java项目 seleniumDemo&#xff0c;并降解压的文件放入依赖中&#xff08;1&#xff09;双击项目的src打开项目结构&#xff0c;或右键-打开…...

分类评估指标

文章目录 1. 混淆矩阵2. Precision(精准率)3. Recall(召回率)4. F1-score5. ROC曲线和AUC指标5.1 ROC 曲线5.2 绘制 ROC 曲线5.3 AUC 值6. API介绍6.1 **分类评估报告api**6.2 **AUC计算API**练习-电信客户流失预测1. 数据集介绍2. 处理流程3. 案例实现4. 小结1. 混淆矩阵 …...

OpenCV:图像直方图计算

图像直方图为图像中像素强度的分布提供了有价值的见解。通过了解直方图&#xff0c;你可以获得有关图像对比度、亮度和整体色调分布的信息。这些知识对于图像增强、图像分割和特征提取等任务非常有用。 本文旨在为学习如何使用 OpenCV 执行图像直方图计算提供清晰且全面的指南。…...

用QFramework来重构 祖玛游戏

资料 Unity - 祖玛游戏 GitHub 说明 用QF一个场景就够了&#xff0c;在UIRoot下切换预制体达到面板切换。 但测试中当然要有一个直接跳到测试面板的 测试脚本&#xff0c;保留测试Scene&#xff08;不然初学者也不知道怎么恢复测试Scene&#xff09;&#xff0c;所以全文按S…...

生活杂记-显示器尺寸

以下是常见显示器尺寸的对角线长度换算成厘米的结果&#xff08;已经四舍五入到最接近的厘米数&#xff09;&#xff1a; 19英寸显示器 ≈ 48.26厘米21.5英寸显示器 ≈ 54.61厘米24英寸显示器 ≈ 60.96厘米27英寸显示器 ≈ 68.58厘米32英寸显示器 ≈ 81.28厘米34英寸显示器 ≈…...

在CSDN学Golang云原生(Kubernetes Pod无状态部署)

一&#xff0c;静态pod Kubernetes中的Pod是可以动态创建、销毁的&#xff0c;如果希望Pod只使用静态的IP地址而不是自动生成一个IP地址&#xff0c;那么就需要使用静态Pod。 静态Pod是在kubelet启动时通过指定文件夹路径来加载的。当kubelet检测到这些配置文件变化后&#x…...

@Bean的作用

Bean通常和Configuration注解一起使用 Bean可以用在方法上&#xff0c;方法返回的对象交给spring容器管理&#xff0c;和提供给其他程序组件使用 Bean是一个注解&#xff0c;用于将方法标记为Spring容器中的一个Bean。具体来说&#xff0c;Bean注解可以用于方法上&#xff0c…...

【论文阅读22】Label prompt for multi-label text classification

论文相关 论文标题&#xff1a;Label prompt for multi-label text classification&#xff08;基于提示学习的多标签文本分类&#xff09; 发表时间&#xff1a;2023 领域&#xff1a;多标签文本分类 发表期刊&#xff1a;Applied Intelligence&#xff08;SCI二区&#xff0…...

EasyExcel数据导出功能封装

起因: 最近需要用到excel导出功能,使用EasyExcel可以快速实现导出,又需要优雅的对EasyExcel进行封装,在实现自己的导出功能时又可以制定一定的规则,让其他同事方便使用,最近研究了下网上的常规写法,站在巨人的肩上重新添加了自己的思路,供大家参考,有任何问题请多指教…...

通过web.xml来配置servlet程序

IDEA 2022.3.3 tomcat-9.0.27 Java EE8 JDK-16 配置访问的虚拟路径 web.xml <?xml version"1.0" encoding"UTF-8"?> <web-app xmlns"http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi"http://www.w3.org/2001/XMLSchema-insta…...

umi 创建的项目中,如何配置多个环境变量

创建env.js 在config.js中配置 在页面中使用 env.js和config.js的目录顺序 package.json中的配置...

Mysql 5.7 连接数爆满 清理连接数

Mysql 5.7 连接数爆满 清理连接数 我在做项目的时候遇到了这个报错&#xff0c;然后搜了半天也没有在网上找到mysql清理连接数的方案&#xff0c;后面还是自己写了一个 打开MySQL命令行或客户端&#xff0c;并使用管理员权限登录到MySQL服务器。 我这里使用的是navicat 输入…...

HTTPS工作原理

先简述一下什么是HTTPS&#xff0c;HTTPS就是在HTTP的基础上增加了SSL/TLS来完成加密传输&#xff0c;以免敏感信息被第三方获取&#xff0c;所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。 一、客户端发起HTTPS请求 这个没什么好说的&#xff0c;就是…...

十大基础算法

一、选择排序 过程简单描述&#xff1a; 首先&#xff0c;找到数组中最小的那个元素&#xff0c;其次&#xff0c;将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。其次&#xff0c;在剩下的元素中找到最小的元素&#xff0c;将它与数组的第二…...

Java---第八章(字符串-----String,StringBuilder 和 StringBuffer)

Java---第八章 字符串String字符串的常用方法StringBuilder和StringBuffer常用方法 对比String 和StringBuilder 和 StringBuffer 字符串 String 特性&#xff1a; String 类位于java.lang包中&#xff0c;无需引入&#xff0c;可直接使用String 类是由final修饰的&#xff…...

k8s集群的部署

【1】安装docker systemctl enable docker所有节点均需要安装docker,并且使其开机自启&#xff0c;每个节点均部署镜像加速器 【2】配置k8s的yum文件 [rootk8s1 ~]# cd /etc/yum.repos.d/ [rootk8s1 yum.repos.d]# vim k8s.repo [rootk8s1 yum.repos.d]# cat k8s.repo [k8s…...

设计模式——观察者模式

文章目录 1 概述2 实现3 总结 1 概述 观察者模式可以分为观察者和被观察者&#xff0c;观察者通过注册到一个被观察者中&#xff0c;也可视为订阅&#xff0c;当被观察者的数据发生改变时&#xff0c;会通知到观察者&#xff0c;观察者可以据此做出反应。 可以类比订阅报纸&am…...

在Debian 12 上安装 PHP 5.6, 7.4

环境&#xff1a;Debian 12 Debian 12 默认的PHP版本为 8.2 如果直接安装php7.4就出现下面的报错&#xff1a; sudo apt-get install libapache2-mod-php7.4 php7.4 php7.4-gd php7.4-opcache php7.4-mbstring php7.4-xml php7.4-json php7.4-zip php7.4-curl php7.4-imap p…...

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站&#xff0c;会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后&#xff0c;网站没有变化的情况。 不熟悉siteground主机的新手&#xff0c;遇到这个问题&#xff0c;就很抓狂&#xff0c;明明是哪都没操作错误&#x…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

Day131 | 灵神 | 回溯算法 | 子集型 子集

Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 笔者写过很多次这道题了&#xff0c;不想写题解了&#xff0c;大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容

基于 ​UniApp + WebSocket​实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配​微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

JVM虚拟机:内存结构、垃圾回收、性能优化

1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

Web后端基础(基础知识)

BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程序的逻辑和数据都存储在服务端。 优点&#xff1a;维护方便缺点&#xff1a;体验一般 CS架构&#xff1a;Client/Server&#xff0c;客户端/服务器架构模式。需要单独…...

苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会

在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...

深入理解Optional:处理空指针异常

1. 使用Optional处理可能为空的集合 在Java开发中&#xff0c;集合判空是一个常见但容易出错的场景。传统方式虽然可行&#xff0c;但存在一些潜在问题&#xff1a; // 传统判空方式 if (!CollectionUtils.isEmpty(userInfoList)) {for (UserInfo userInfo : userInfoList) {…...

mac:大模型系列测试

0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何&#xff0c;是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试&#xff0c;是可以跑通文章里面的代码。训练速度也是很快的。 注意…...