乐观锁与悲观锁的实现和应用
乐观锁与悲观锁:原理、实现与应用详解
在并发编程和数据库操作中,乐观锁和悲观锁是两种重要的并发控制策略,它们在原理、实现方式和应用场景上存在显著差异。下面我们将通过图文结合的方式,深入探讨这两种锁机制。
一、基本概念
1.1 悲观锁
悲观锁的核心思想是 先锁后用,它认为在数据处理过程中,很可能会发生并发冲突。因此,在进行数据操作之前,就会获取锁,以确保在当前事务处理期间,其他事务无法对同一数据进行修改,从而保证数据的一致性和完整性。
1.2 乐观锁
乐观锁秉持 先试后验 的理念,它假定在大多数情况下,数据处理过程中不会发生冲突,所以不会在操作数据前加锁。只有在更新数据时,才会去验证在本次更新之前,是否有其他事务对数据进行了修改。如果没有修改,则执行更新操作;如果数据已被修改,则采取相应的处理措施(如重试、回滚等)。
二、实现方式
2.1 悲观锁的实现
2.1.1 数据库层面
在数据库中,常使用SELECT ... FOR UPDATE语句实现悲观锁。该语句会对查询到的数据加上排它锁(X 锁),阻止其他事务对数据进行读写操作,直到当前事务提交或回滚。
-- 假设存在账户表accounts,包含id, balance字段
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL
);
-- 插入测试数据
INSERT INTO accounts (id, balance) VALUES (1, 1000.00);
-- 事务1:扣款操作
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 假设查询结果: id=1, balance=1000.00
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 事务2:在事务1提交前尝试扣款
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 此查询会被阻塞,直到事务1提交或回滚
2.1.2 编程语言层面
在 Java 中,可使用synchronized关键字和ReentrantLock类实现悲观锁;Python 提供了threading.Lock类。这些工具通过互斥访问的方式,保证同一时刻只有一个线程能访问共享资源。
public class PessimisticLockExample {
private final ReentrantLock lock = new ReentrantLock();
private double balance = 1000.0;
public void withdraw(double amount) {
lock.lock();
try {
// 模拟业务处理时间
Thread.sleep(100);
if (balance >= amount) {
balance -= amount;
System.out.println("扣款成功,余额: " + balance);
} else {
System.out.println("余额不足");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
PessimisticLockExample account = new PessimisticLockExample(); // 模拟两个线程同时扣款
Thread t1 = new Thread(() -> account.withdraw(500));
Thread t2 = new Thread(() -> account.withdraw(800)); t1.start();
t2.start(); t1.join();
t2.join(); System.out.println("最终余额: " + account.balance);
}
}
2.2 乐观锁的实现
2.2.1 版本号机制
在数据库表中添加一个version字段,每次数据更新时,该字段值递增。更新数据前,先比较当前事务读取的version值与数据库中的version值,若一致则执行更新,并将version值加 1;若不一致,则说明数据已被其他事务修改,本次更新失败。
-- 假设账户表accounts包含id, balance, version字段
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL,
version INT DEFAULT 0
);
-- 插入测试数据
INSERT INTO accounts (id, balance) VALUES (1, 1000.00);
-- 事务1:更新操作
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
-- 返回结果: id=1, balance=1000.00, version=0
UPDATE accounts
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 0;
-- 如果执行成功,affected rows = 1,version变为1
COMMIT;
-- 事务2:并发更新操作
BEGIN TRANSACTION;
SELECT * FROM accounts WHERE id = 1;
-- 返回结果: id=1, balance=1000.00, version=0 (因为在事务1提交前读取)
UPDATE accounts
SET balance = balance - 200, version = version + 1
WHERE id = 1 AND version = 0;
-- 执行失败,affected rows = 0,因为version已经被事务1更新为1
-- 处理更新失败的逻辑
IF ROW_COUNT() = 0 THEN
-- 重试或回滚
ROLLBACK;
END IF;
COMMIT;
2.2.2 时间戳机制
与版本号机制类似,时间戳机制使用数据的最后修改时间来判断数据是否被修改。更新数据时,验证时间戳是否发生变化,若变化则更新失败。
2.2.3 CAS(Compare-and-Swap)操作
CAS 是一种无锁的原子操作,在编程语言和硬件层面均有支持。它包含三个操作数:内存地址(V)、预期原值(A)和新值(B)。仅当内存地址 V 中的值与预期原值 A 相等时,才将内存地址 V 中的值更新为新值 B。
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLockExample {
private AtomicInteger balance = new AtomicInteger(1000);
public boolean withdraw(double amount) {
int oldValue;
int newValue;
do {
oldValue = balance.get();
if (oldValue < amount) {
System.out.println("余额不足");
return false;
}
newValue = (int) (oldValue - amount);
// 模拟CAS操作前的竞争
Thread.yield();
} while (!balance.compareAndSet(oldValue, newValue)); System.out.println("扣款成功,余额: " + balance.get());
return true;
}
public static void main(String[] args) throws InterruptedException {
OptimisticLockExample account = new OptimisticLockExample(); // 模拟两个线程同时扣款
Thread t1 = new Thread(() -> account.withdraw(500));
Thread t2 = new Thread(() -> account.withdraw(800)); t1.start();
t2.start(); t1.join();
t2.join(); System.out.println("最终余额: " + account.balance.get());
}
}
三、性能对比与应用场景
3.1 性能对比
特性 | 悲观锁 | 乐观锁 |
适用场景 | 写操作频繁、冲突可能性高的情况 | 读操作频繁、冲突可能性低的情况 |
加锁时机 | 在操作数据之前就加锁 | 在更新数据的时候才验证 |
性能表现 | 会导致较多的锁等待现象,性能开销较大 | 无需加锁,性能开销较小 |
实现复杂度 | 相对简单 | 相对复杂,需要处理更新失败的情况 |
典型应用 | 数据库的行锁、表锁 | 数据库的版本号、CAS 操作 |
为了更直观地感受性能差异,可通过以下 Java 代码进行测试:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class LockPerformanceTest {
private static final int THREAD_COUNT = 10;
private static final int OPS_PER_THREAD = 100000;
// 悲观锁测试
static class PessimisticCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
// 乐观锁测试
static class OptimisticCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public static void main(String[] args) throws InterruptedException {
testPessimisticLock();
testOptimisticLock();
}
private static void testPessimisticLock() throws InterruptedException {
PessimisticCounter counter = new PessimisticCounter();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
long startTime = System.nanoTime();
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < OPS_PER_THREAD; j++) {
counter.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long endTime = System.nanoTime();
System.out.println("悲观锁耗时: " + (endTime - startTime) / 1_000_000 + " ms");
System.out.println("最终计数: " + counter.getCount());
}
private static void testOptimisticLock() throws InterruptedException {
OptimisticCounter counter = new OptimisticCounter();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
long startTime = System.nanoTime();
for (int i = 0; i < THREAD_COUNT; i++) {
executor.submit(() -> {
for (int j = 0; j < OPS_PER_THREAD; j++) {
counter.increment();
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
long endTime = System.nanoTime();
System.out.println("乐观锁耗时: " + (endTime - startTime) / 1_000_000 + " ms");
System.out.println("最终计数: " + counter.getCount());
}
}
3.2 应用场景
- 悲观锁:适用于银行转账、库存扣减等对数据一致性要求极高,且写操作频繁、冲突可能性大的场景。
- 乐观锁:常用于商品浏览计数、论坛帖子浏览量统计等读多写少,对性能要求较高,且允许一定概率更新失败的场景。
通过以上对乐观锁和悲观锁的原理剖析、实现示例、性能对比以及应用场景分析,我们对这两种并发控制策略有了更全面深入的理解。在实际开发中,应根据具体业务需求,合理选择合适的锁机制,以实现高效、可靠的并发处理。
相关文章:
乐观锁与悲观锁的实现和应用
乐观锁与悲观锁:原理、实现与应用详解 在并发编程和数据库操作中,乐观锁和悲观锁是两种重要的并发控制策略,它们在原理、实现方式和应用场景上存在显著差异。下面我们将通过图文结合的方式,深入探讨这两种锁机制。 一、基本概念 1…...

PL/SQLDeveloper中数值类型字段查询后显示为科学计数法的处理方式
PL/SQLDeveloper中数值类型字段查询后显示为科学计数法的处理方式 文章目录 PL/SQLDeveloper中数值类型字段查询后显示为科学计数法的处理方式1. 查询效果2. 处理方式3. 再次查询 1. 查询效果 2. 处理方式 3. 再次查询...

【vue】Uniapp 打包Android 文件选择上传问题详解~
需求 uniapp兼容android app,pc,h5的文件选择并上传功能。 需要支持拍照和相册选择,以及选择其他类型文件上传~ 实践过程和问题 开始使用uni-file-picker组件 以为很顺利,android模拟器测试…… 忽略了平台兼容性提示~&#…...
ASR技术(自动语音识别)深度解析
ASR技术(自动语音识别)深度解析 自动语音识别(Automatic Speech Recognition,ASR)是将人类语音转换为文本的核心技术,以下是其全面解析: 一、技术原理架构 #mermaid-svg-QlJOWpMtlGi9LNeF {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:1…...
图论水题2
div2 361 D. Tree Requests 题意 对于一颗 n n n节点的树,每个节点有一个字母,有 m m m次询问,每次询问求对于顶点 v v v的子树中深度为 h h h的结点能否组成一个回文串$ (1 \leq n \leq m \leq 5 \cdot 10^5) $ 思路 关于 v v v的子树结…...

Ctrl-Crash 助力交通安全:可控生成逼真车祸视频,防患于未然
视频扩散技术虽发展显著,但多数驾驶数据集事故事件少,难以生成逼真车祸图像,而提升交通安全又急需逼真可控的事故模拟。为此,论文提出可控车祸视频生成模型 Ctrl-Crash,它以边界框、碰撞类型、初始图像帧等为条件&…...

网络编程之服务器模型与UDP编程
一、服务器模型 在网络通信中,通常要求一个服务器连接多个客户端 为了处理多个客户端的请求,通常有多种表现形式 1、循环服务器模型 一个服务器可以连接多个客户端,但同一时间只能连接并处理一个客户的请求 socket() 结构体 bind() listen() …...

Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测
Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测 目录 Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五模型时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Transformer-BiLSTM、Transformer、CNN-BiLSTM、BiLSTM、CNN五…...

阿里云服务器安装nginx并配置前端资源路径(前后端部署到一台服务器并成功访问)
运行以下命令,安装Nginx相关依赖。 yum install -y gcc-c yum install -y pcre pcre-devel yum install -y zlib zlib-devel yum install -y openssl openssl-devel 运行wget命令下载Nginx 1.21.6。 您可以通过Nginx开源社区直接获取对应版本的安装包URL&…...
Ubuntu 下开机自动执行命令的方法
Ubuntu 下开机自动执行命令的方法(使用 crontab) 在日常使用 Ubuntu 或其他 Linux 系统时,我们常常需要让某些程序或脚本在系统启动后自动运行。例如:启动 Clash 代理、初始化服务、定时同步数据等。 本文将介绍一种简单且常用的…...

C++11新增重要标准(下)
前言 一,forward(完美转发) 二,可变参数模板 三,emplace系列接口 四,新增类功能 五,default与delete 六,lambda表达式 七,包装器 八,bind 在C11中新增…...

【第六篇】 SpringBoot的日志基础操作
简介 日志系统在软件开发中至关重要,用于调试代码、记录运行信息及错误堆栈。本篇文章不仅详细介绍了日志对象的创建及快速使用,还说明了日志持久化的两种配置方式和滚动日志的设置。实际开发需根据场景选择合适的日志级别和存储策略。文章内容若存在错误…...

Pluto论文阅读笔记
主要还是参考了这一篇论文笔记:https://zhuanlan.zhihu.com/p/18319150220 Pluto主要有三个创新点: 横向纵向用lane的query来做将轨迹投回栅格化地图,计算碰撞loss对数据进行正增强和负增强,让正增强的结果也无增强的结果相近&a…...
ubuntu显示器未知
在Ubuntu系统中,当外接显示器被识别为“未知设备”时,可通过以下日志文件进行问题诊断,结合Xorg日志和内核日志综合分析: 🔍 一、查看Xorg显示服务日志(核心) Xorg日志记录了图形界面的详细事…...
Faiss向量数据库全面解析:从原理到实战
Faiss向量数据库全面解析:从原理到实战 引言:向量搜索的时代需求 在AI技术爆发的今天,向量数据已成为表示文本、图像、音视频等内容的核心形式。Facebook AI研究院开源的Faiss(Facebook AI Similarity Search)作为高…...

matlab 2024a 工具箱Aerospsce Toolbox报错
Matlab R2024a中Aerospsce Toolbox报错 警告:Aerospace Toolbox and Aerospace Blockset licenses are required in ‘built-in/Spacecraft Dynamics’ 找到安装路径\MATLAB\R2024a\licenses文件夹license_****_R2024a.lic 里面工具箱名称出错,手动修改…...

使用有限计算实现视频生成模型的高效训练
大家读完觉得有帮助记得关注和点赞!!! 抽象 视频生成的最新进展需要越来越高效的训练配方,以减轻不断上升的计算成本。在本报告中,我们介绍了 ContentV,这是一种 8B 参数文本到视频模型,在 256 …...

Server2003 B-1 Windows操作系统渗透
任务环境说明: 服务器场景:Server2003(开放链接) 服务器场景操作系统:Windows7 1.通过本地PC中渗透测试平台Kali对服务器场景Windows进行系统服务及版本扫描渗透测试,并将该操作显示结果中Telnet服务对应的…...

一次Oracle的非正常关闭
数据库自己会关闭吗? 从现象来说Oracle MySQL Redis等都会出现进程意外停止的情况。而这些停止都是非人为正常关闭或者暴力关闭(abort或者kill 进程) 一次测试环境的非关闭 一般遇到这种情况先看一下错误日志吧。 2025-06-01T06:26:06.35…...
AI不会杀死创作,但会杀死平庸
作为一个敲了8年Java代码的普通本科程序员,日常主要泡在会议后台管理系统的开发里。从2023年底被朋友拽着试了第一把AI工具到现在,电脑手机上的AI软件比外卖App还多——写代码的Copilot、画时序图的工具、聊天的ChatGPT、Deepseek,基本市面上…...
JeecgBoot低代码管理平台
一、一句话理解 JeecgBoot JeecgBoot 是一个基于 Java 技术栈(主要是 Spring Boot 和 Vue)的快速开发脚手架。它的核心理念是:通过代码生成器和一系列预置模块,极大地减少程序员在开发企业级后台管理系统时重复的、模板化的工作&…...
Fetch与Axios:区别、联系、优缺点及使用差异
Fetch与Axios:区别、联系、优缺点及使用差异 文章目录 Fetch与Axios:区别、联系、优缺点及使用差异一、联系二、区别1. 浏览器支持与兼容性2. 响应处理3. 请求拦截和响应拦截4. 错误处理 三、优缺点1. Fetch API优点缺点 2. Axios优点缺点 四、使用上的差…...

YOLO11解决方案之分析
概述 Ultralytics提供了一系列的解决方案,利用YOLO11解决现实世界的问题,包括物体计数、模糊处理、热力图、安防系统、速度估计、物体追踪等多个方面的应用。 Ultralytics提供了三种基本的数据可视化类型:折线图(面积图…...

yolov11与双目测距结合,实现目标的识别和定位测距(onnx版本)
一、yolov11双目测距基本流程 yolov11 双目测距的大致流程就是: 双目标定 --> 立体校正(含消除畸变) --> 立体匹配 --> 视差计算 --> 深度计算(3D坐标)计算 --> 目标检测 --> 目标距离计算及可视化 下面将分别阐述每…...

基于51单片机和8X8点阵屏、独立按键的填充消除类小游戏
目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、8X8点阵屏2、独立按键3、定时器04、定时器1 四、主函数总结 系列文章目录 前言 使用的是普中A2开发板。 【单片机】STC89C52RC 【频率】12T11.0592MHz 【外设】8X8点阵屏、独立按键 效果查看/操作演示&#x…...
将数据库表导出为C#实体对象
数据库方式 use 数据库;declare TableName sysname 表名 declare Result varchar(max) /// <summary> /// TableName /// </summary> public class TableName {select Result Result /// <summary>/// CONVERT(NVARCHAR(500), ISNULL(ColN…...

物联网技术发展与应用研究分析
文章目录 引言一、物联网的基本架构(一)感知层(二)网络层(三)平台层(四)应用层 二、物联网的关键技术(一)传感器技术(二)通信技术&…...

金融系统渗透测试
金融系统渗透测试是保障金融机构网络安全的核心环节,它的核心目标是通过模拟攻击手段主动发现系统漏洞,防范数据泄露、资金盗取等重大风险。 一、金融系统渗透测试的核心框架 合规性驱动 需严格遵循《网络安全法》《数据安全法》及金融行业监管要求&am…...
C++ 信息学奥赛总复习题
第一章 C 基础语法 一、填空题 C 源文件的扩展名通常是______。C 程序的入口函数是______。在 C 中,注释有两种形式,分别是______和______。声明一个整型变量 a 的语句是______。输出语句的关键字是______。 二、判断题 C 区分大小写。( …...

9.进程间通信
1.简介 为啥要有进程间通信? 如果未来进程之间要协同呢?一个进程要把自己的数据交给另一个进程!进程是具有独立性的,所以把一个进程的数据交给另一个进程----基本不可能!必须通信起来,就必须要有另一个人…...