乐观锁与悲观锁的实现和应用

news2025/6/8 19:03:57

乐观锁与悲观锁:原理、实现与应用详解

在并发编程和数据库操作中,乐观锁和悲观锁是两种重要的并发控制策略,它们在原理、实现方式和应用场景上存在显著差异。下面我们将通过图文结合的方式,深入探讨这两种锁机制。

一、基本概念

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 应用场景

  • 悲观锁:适用于银行转账、库存扣减等对数据一致性要求极高,且写操作频繁、冲突可能性大的场景。
  • 乐观锁:常用于商品浏览计数、论坛帖子浏览量统计等读多写少,对性能要求较高,且允许一定概率更新失败的场景。

通过以上对乐观锁和悲观锁的原理剖析、实现示例、性能对比以及应用场景分析,我们对这两种并发控制策略有了更全面深入的理解。在实际开发中,应根据具体业务需求,合理选择合适的锁机制,以实现高效、可靠的并发处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2404454.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PL/SQLDeveloper中数值类型字段查询后显示为科学计数法的处理方式

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

【vue】Uniapp 打包Android 文件选择上传问题详解~

需求 uniapp兼容android app&#xff0c;pc&#xff0c;h5的文件选择并上传功能。 需要支持拍照和相册选择&#xff0c;以及选择其他类型文件上传~ 实践过程和问题 开始使用uni-file-picker组件 以为很顺利&#xff0c;android模拟器测试…… 忽略了平台兼容性提示~&#…

Ctrl-Crash 助力交通安全:可控生成逼真车祸视频,防患于未然

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

网络编程之服务器模型与UDP编程

一、服务器模型 在网络通信中&#xff0c;通常要求一个服务器连接多个客户端 为了处理多个客户端的请求&#xff0c;通常有多种表现形式 1、循环服务器模型 一个服务器可以连接多个客户端&#xff0c;但同一时间只能连接并处理一个客户的请求 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并配置前端资源路径(前后端部署到一台服务器并成功访问)

​​​运行以下命令&#xff0c;安装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&…

C++11新增重要标准(下)

前言 一&#xff0c;forward&#xff08;完美转发&#xff09; 二&#xff0c;可变参数模板 三&#xff0c;emplace系列接口 四&#xff0c;新增类功能 五&#xff0c;default与delete 六&#xff0c;lambda表达式 七&#xff0c;包装器 八&#xff0c;bind 在C11中新增…

【第六篇】 SpringBoot的日志基础操作

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

Pluto论文阅读笔记

主要还是参考了这一篇论文笔记&#xff1a;https://zhuanlan.zhihu.com/p/18319150220 Pluto主要有三个创新点&#xff1a; 横向纵向用lane的query来做将轨迹投回栅格化地图&#xff0c;计算碰撞loss对数据进行正增强和负增强&#xff0c;让正增强的结果也无增强的结果相近&a…

matlab 2024a ​工具箱Aerospsce Toolbox报错​

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

使用有限计算实现视频生成模型的高效训练

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

Server2003 B-1 Windows操作系统渗透

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

一次Oracle的非正常关闭

数据库自己会关闭吗&#xff1f; 从现象来说Oracle MySQL Redis等都会出现进程意外停止的情况。而这些停止都是非人为正常关闭或者暴力关闭&#xff08;abort或者kill 进程&#xff09; 一次测试环境的非关闭 一般遇到这种情况先看一下错误日志吧。 2025-06-01T06:26:06.35…

YOLO11解决方案之分析

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

yolov11与双目测距结合,实现目标的识别和定位测距(onnx版本)

一、yolov11双目测距基本流程 yolov11 双目测距的大致流程就是&#xff1a; 双目标定 --> 立体校正&#xff08;含消除畸变&#xff09; --> 立体匹配 --> 视差计算 --> 深度计算(3D坐标)计算 --> 目标检测 --> 目标距离计算及可视化 下面将分别阐述每…

基于51单片机和8X8点阵屏、独立按键的填充消除类小游戏

目录 系列文章目录前言一、效果展示二、原理分析三、各模块代码1、8X8点阵屏2、独立按键3、定时器04、定时器1 四、主函数总结 系列文章目录 前言 使用的是普中A2开发板。 【单片机】STC89C52RC 【频率】12T11.0592MHz 【外设】8X8点阵屏、独立按键 效果查看/操作演示&#x…

物联网技术发展与应用研究分析

文章目录 引言一、物联网的基本架构&#xff08;一&#xff09;感知层&#xff08;二&#xff09;网络层&#xff08;三&#xff09;平台层&#xff08;四&#xff09;应用层 二、物联网的关键技术&#xff08;一&#xff09;传感器技术&#xff08;二&#xff09;通信技术&…

金融系统渗透测试

金融系统渗透测试是保障金融机构网络安全的核心环节&#xff0c;它的核心目标是通过模拟攻击手段主动发现系统漏洞&#xff0c;防范数据泄露、资金盗取等重大风险。 一、金融系统渗透测试的核心框架 合规性驱动 需严格遵循《网络安全法》《数据安全法》及金融行业监管要求&am…

9.进程间通信

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

React 基础入门笔记

一、JSX语法规则 1. 定义虚拟DOM时&#xff0c;不要写引号 2.标签中混入JS表达式时要用 {} &#xff08;1&#xff09;.JS表达式与JS语句&#xff08;代码&#xff09;的区别 &#xff08;2&#xff09;.使用案例 3.样式的类名指定不要用class&#xff0c;要用className 4.内…