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

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

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

一、基本概念

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.pswp.cn/web/82919.shtml

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

相关文章

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…

Redis主从复制原理二 之 主从复制工作流程

概述 本文紧接「Redis主从复制的原理一 之 概述」&#xff0c;详细介绍了Redis的主从服务过程及原理。 主从复制工作流程 主从复制过程大体可以分为3个阶段&#xff1a; 建立连接阶段&#xff08;即准备阶段&#xff09;数据同步阶段命令传播阶段 阶段一&#xff1a;建立连接阶…

Markdown基础(1.2w字)

1. Markdown基础 这次就没目录了&#xff0c;因为md格式太乱了写示例&#xff0c;展示那些都太乱了&#xff0c;导致目录很乱。 &#xff08;我是XX&#xff0c;出现了很多错误&#xff0c;有错误和我说&#xff09; 1.1 Markdown简介 Markdown是一种轻量级的标记语言&#…

JAVA-springboot log日志

SpringBoot从入门到精通-第8章 日志的操作 一、Spring Boot默认的日志框架 SpringBoot支持很多种日志框架&#xff0c;通常情况下&#xff0c;这些日志框架都是由一个日志抽象层和一个日志实现层搭建而成的&#xff0c;日志抽象层是为记录日志提供的一套标准且规范的框架&…

Vue 渲染 Markdown 文件完全指南

前言 大家好&#xff0c;我是一诺&#xff0c;今天分享的是vue中渲染markdown文件。这是一个常见的需求&#xff0c;比如用户隐私协议页、技术说明等文档页面~ 本文将详细介绍如何在 Vue 中渲染 Markdown 文件&#xff0c;并美化代码块的显示效果。 基础概念 什么是 Markdo…

Science Robotics:UCLA 贺曦敏团队综述自主软体机器人

在机器人中实现类似生命的自主性一直是研究的方向&#xff0c;但目前大多数软体机器人仍依赖外部刺激操控来产生持续运动。为了实现能够自我调节感知 、 决策和驱动的自主物理智能&#xff08;autonomous physical intelligence&#xff0c;API&#xff09;&#xff0c;一种有前…

基于LangChain构建高效RAG问答系统:向量检索与LLM集成实战

基于LangChain构建高效RAG问答系统&#xff1a;向量检索与LLM集成实战 在本文中&#xff0c;我将详细介绍如何使用LangChain框架构建一个完整的RAG&#xff08;检索增强生成&#xff09;问答系统。通过向量检索获取相关上下文&#xff0c;并结合大语言模型&#xff0c;我们能够…

【Java学习笔记】SringBuffer类(重点)

StringBuffer&#xff08;重点&#xff09; 1. 基本介绍 &#xff08;1&#xff09;StringBuffer是可变的字符序列&#xff0c;可以对字符串内容惊醒增删 &#xff08;2&#xff09;很多方法喝String相同&#xff0c;但StringBuffer可变长度 &#xff08;3&#xff09;Strin…

计算机网络领域所有CCF-A/B/C类期刊汇总!

本期小编统计了【计算机网络】领域CCF推荐所有期刊的最新影响因子&#xff0c;分区、年发文量以及投稿经验&#xff0c;供大家参考&#xff01; CCF-A类 1 IEEE Journal on Selected Areas in Communications 【影响因子】13.8 【期刊分区】JCR1区&#xff0c;中科院1区TOP …

AI-Sphere-Butler之如何启动AI全能管家教程(WSL测试环境下适用)

环境&#xff1a; Ubuntu20.04 WSL2 问题描述&#xff1a; AI-Sphere-Butler之如何启动AI全能管家教程(WSL测试环境下适用) 解决方案&#xff1a; 打开管家大模型 1.运行大模型在cmd下输入&#xff1a; ollama run qwen2.5-3bnsfwny运行管家 数字人运行脚本&#xff…

【python深度学习】Day 47 注意力热图可视化

知识点&#xff1a;热力图 作业&#xff1a;对比不同卷积层热图可视化的结果 一、概念 为了方便观察输出&#xff0c;将特征图进行可视化。特征图本质就是不同的卷积核的输出&#xff0c;浅层指的是离输入图近的卷积层&#xff0c;浅层卷积层的特征图通常较大&#xff0c;而深层…

C#语音识别:使用Whisper.net实现语音识别

C#语音识别&#xff1a;使用Whisper.net实现语音识别 在当今数字化时代&#xff0c;语音识别技术已广泛应用于智能助手、语音转文字、会议记录等众多领域。对于 C# 开发者而言&#xff0c;如何快速、高效地实现语音识别功能呢&#xff1f;今天&#xff0c;我们就来介绍一个强大…

开源分享|适合初创商家的餐饮系统,基于thinkphp8+element-plus

一、项目介绍 三勾餐饮点餐连锁版系统是一个基于thinkphp8element-plusuniapp打造的面向开发的小程序商城的全面解决方案&#xff0c;旨在为连锁餐饮企业提供高效的点餐与管理服务。该系统支持多端应用发布&#xff0c;包括微信小程序、H5、安卓及iOS平台&#xff0c;实现数据…

rec_pphgnetv2完整代码学习(一)

rec_pphgnetv2是paddleocr_v5中的重要改进&#xff0c;因此对其完整代码进行学习十分之有必要。 一、IdentityBasedConv1x1 这段代码定义了 IdentityBasedConv1x1 类&#xff0c;它是 PaddleOCRv5 中 rec_pphgnetv2 模型的关键改进之一。该层通过将恒等映射&#xff08;Ident…

vue3+dify从零手撸AI对话系统

vue3dify从零手撸AI对话系统 前言 近年来&#xff0c;人工智能技术呈现爆发式增长&#xff0c;其应用已深度渗透至各行各业。甚至家里长辈们也开始借助AI工具解决日常问题。作为程序员群体&#xff0c;我们更应保持技术敏锐度&#xff0c;紧跟这波浪潮。 回溯求学时期&#xf…

robot_lab train的整体逻辑

Go2机器人推理(Play)流程详细分析 概述 本文档详细分析了使用命令 python scripts/rsl_rl/base/play.py --task RobotLab-Isaac-Velocity-Rough-Unitree-Go2-v0 进行Go2机器人推理的完整流程&#xff0c;基于实际的代码实现&#xff0c;包括模型加载、环境配置调整、推理循环…

Python Day45

Task&#xff1a; 1.tensorboard的发展历史和原理 2.tensorboard的常见操作 3.tensorboard在cifar上的实战&#xff1a;MLP和CNN模型 效果展示如下&#xff0c;很适合拿去组会汇报撑页数&#xff1a; 作业&#xff1a;对resnet18在cifar10上采用微调策略下&#xff0c;用tens…

MySQL SQL 优化:从 INSERT 到 LIMIT 的实战与原理

在数据库驱动的现代应用中&#xff0c;SQL 查询的性能直接决定了用户体验和系统效率。本文将深入探讨 MySQL &#xff08;特别是 InnoDB 存储引擎&#xff09;中常见的 SQL 性能瓶颈&#xff0c;并结合实际案例&#xff0c;详细剖析从数据插入到复杂分页查询的优化策略与底层实…

SQL 基础入门

SQL 基础入门 SQL&#xff08;全称 Structured Query Language&#xff0c;结构化查询语言&#xff09;是用于操作关系型数据库的标准语言&#xff0c;主要用于数据的查询、新增、修改和删除。本文面向初学者&#xff0c;介绍 SQL 的基础概念和核心操作。 1. 常见的 SQL 数据…

HTTP 请求协议简单介绍

目录 常见的 HTTP 响应头字段 Java 示例代码&#xff1a;发送 HTTP 请求并处理响应 代码解释&#xff1a; 运行结果&#xff1a; 文件名&#xff1a; 总结&#xff1a; HTTP&#xff08;HyperText Transfer Protocol&#xff09;是用于客户端与服务器之间通信的协议。它定…