锁的艺术:深入浅出讲解乐观锁与悲观锁

news2025/6/8 20:48:17

        在多线程和分布式系统中,数据一致性是一个核心问题。锁机制作为解决并发冲突的重要手段,被广泛应用于各种场景。乐观锁悲观锁是两种常见的锁策略,它们在设计理念、实现方式和适用场景上各有特点。本文将深入探讨乐观锁和悲观锁的原理、实现、优缺点以及具体的应用实例,并结合代码进行详细讲解,帮助读者更好地理解和应用这两种锁机制。

目录

一、锁的基本概念

二、悲观锁

(一)悲观锁的基本概念

(二)悲观锁的特点

(三)悲观锁的实现方式

1. 数据库中的悲观锁

2. Java中的悲观锁

(四)悲观锁的优缺点

三、乐观锁

(一)乐观锁的基本概念

(二)乐观锁的特点

(三)乐观锁的实现方式

1. 基于版本号的乐观锁

2. 基于时间戳的乐观锁

(四)乐观锁的优缺点

四、乐观锁与悲观锁的对比

(一)锁机制

(二)性能

(三)适用场景

五、总结


一、锁的基本概念

        在并发编程中,锁是一种用于控制多个线程对共享资源访问的机制。锁的主要目的是确保在同一时间只有一个线程能够访问共享资源,从而避免数据竞争和不一致问题。锁的实现方式多种多样,但其核心思想是通过某种机制来限制对共享资源的并发访问


二、悲观锁

(一)悲观锁的基本概念

        悲观锁是一种基于“悲观”假设的锁机制。它认为在并发环境中,多个线程对共享资源的访问很可能会发生冲突,因此在访问共享资源之前,会先对资源进行加锁。只有获得锁的线程才能访问资源,其他线程必须等待锁释放后才能继续执行。悲观锁的核心思想是“宁可错杀一千,不可放过一个”,通过严格的锁机制来保证数据的一致性。

(二)悲观锁的特点

  1. 强一致性:悲观锁通过加锁机制严格限制对共享资源的并发访问,能够确保在任何时候只有一个线程能够修改资源,从而保证数据的强一致性。
  2. 高安全性:由于悲观锁在访问资源之前会先加锁,因此可以有效避免数据竞争和并发冲突,适用于对数据一致性要求较高的场景。
  3. 性能瓶颈:悲观锁的加锁和解锁操作会增加系统开销,尤其是在高并发场景下,锁的争用可能导致线程阻塞,降低系统的性能。
  4. 适用场景:悲观锁适用于写操作较多、数据竞争激烈的场景,例如数据库事务中的行锁和表锁。

(三)悲观锁的实现方式

       悲观锁可以通过多种方式实现,常见的有基于数据库的锁机制和基于Java同步原语的锁机制。

1. 数据库中的悲观锁

        在数据库中,悲观锁可以通过SELECT ... FOR UPDATE语句实现。该语句会在查询数据时对数据行加锁,其他事务必须等待锁释放后才能对该行数据进行操作。

-- 查询并锁定一行数据
SELECT * FROM users WHERE id = 1 FOR UPDATE;
  • FOR UPDATE:该子句的作用是锁定查询结果中的行,防止其他事务对该行数据进行修改。

        在Java中,可以通过JDBC操作数据库来实现悲观锁。以下是一个简单的示例代码:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class PessimisticLockExample {
    public static void main(String[] args) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;

        try {
            // 获取数据库连接
            connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "password");
            // 设置事务为非自动提交
            connection.setAutoCommit(false);

            // 查询并锁定一行数据
            String sql = "SELECT * FROM users WHERE id = ? FOR UPDATE";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setInt(1, 1);
            resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                // 获取锁定的数据
                String name = resultSet.getString("name");
                System.out.println("Locked user: " + name);

                // 模拟业务逻辑处理
                Thread.sleep(5000);

                // 更新数据
                String updateSql = "UPDATE users SET name = ? WHERE id = ?";
                preparedStatement = connection.prepareStatement(updateSql);
                preparedStatement.setString(1, "New Name");
                preparedStatement.setInt(2, 1);
                preparedStatement.executeUpdate();

                // 提交事务
                connection.commit();
            }
        } catch (SQLException | InterruptedException e) {
            e.printStackTrace();
            try {
                // 回滚事务
                if (connection != null) {
                    connection.rollback();
                }
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            // 关闭资源
            try {
                if (resultSet != null) {
                    resultSet.close();
                }
                if (preparedStatement != null) {
                    preparedStatement.close();
                }
                if (connection != null) {
                    connection.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

代码说明

  • 使用SELECT ... FOR UPDATE语句查询并锁定数据行。

  • 设置事务为非自动提交模式,确保在事务提交之前,其他事务无法对该行数据进行修改。

  • 在锁定数据后,模拟业务逻辑处理(如Thread.sleep(5000)),然后更新数据并提交事务。

  • 如果发生异常,回滚事务并释放资源。

2. Java中的悲观锁

        在Java中,悲观锁可以通过java.util.concurrent.locks包中的Lock接口及其实现类(如ReentrantLock)来实现。ReentrantLock提供了比内置锁(synchronized)更灵活的锁操作,例如尝试锁定(tryLock)、设置超时时间(tryLock(long timeout, TimeUnit unit))等。

        以下是一个使用ReentrantLock实现悲观锁的示例代码:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final Lock lock = new ReentrantLock();

    public void doSomething() {
        lock.lock(); // 加锁
        try {
            // 模拟业务逻辑
            System.out.println("Thread " + Thread.currentThread().getName() + " is doing something.");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    public static void main(String[] args) {
        ReentrantLockExample example = new ReentrantLockExample();

        // 创建多个线程访问共享资源
        Thread t1 = new Thread(example::doSomething, "Thread-1");
        Thread t2 = new Thread(example::doSomething, "Thread-2");

        t1.start();
        t2.start();
    }
}

代码说明

  • 使用ReentrantLocklock()方法加锁,unlock()方法释放锁。

  • try块中执行业务逻辑,确保在异常情况下能够通过finally块释放锁。

  • 多个线程访问共享资源时,只有获得锁的线程能够执行doSomething方法,其他线程必须等待锁释放。

(四)悲观锁的优缺点

优点

  1. 数据一致性高:悲观锁通过严格的锁机制确保数据的一致性,适用于对数据一致性要求较高的场景。
  2. 实现简单:悲观锁的实现相对简单,尤其是在数据库层面,通过SELECT ... FOR UPDATE语句即可实现。

缺点

  1. 性能瓶颈:悲观锁的加锁和解锁操作会增加系统开销,尤其是在高并发场景下,锁的争用可能导致线程阻塞,降低系统的性能。
  2. 资源利用率低:由于悲观锁限制了并发访问,可能导致资源利用率较低,尤其是在读操作较多的场景下。

三、乐观锁

(一)乐观锁的基本概念

        乐观锁是一种基于“乐观”假设的锁机制。它认为在并发环境中,多个线程对共享资源的访问发生冲突的概率较低,因此在访问资源时不加锁,而是通过其他机制(如版本号或时间戳)来检测数据是否被其他线程修改。如果检测到数据被修改,则放弃当前操作并重试。乐观锁的核心思想是“先做事,再检查”,通过减少锁的使用来提高系统性能。

(二)乐观锁的特点

  1. 高性能:乐观锁减少了锁的使用,降低了锁的开销,适用于读操作较多、写操作较少的场景,能够显著提高系统的性能。
  2. 资源利用率高:乐观锁允许多个线程并发访问共享资源,提高了资源的利用率。
  3. 实现复杂:乐观锁的实现相对复杂,需要通过版本号或时间戳等机制检测数据是否被修改。
  4. 适用场景:乐观锁适用于读操作较多、写操作较少的场景,例如缓存系统、分布式系统中的数据一致性控制。

(三)乐观锁的实现方式

        乐观锁可以通过版本号(Version Number)或时间戳(Timestamp)来实现。以下分别介绍这两种实现方式。

1. 基于版本号的乐观锁

        基于版本号的乐观锁通过为每个数据项添加一个版本号字段来实现。每次修改数据时,版本号加1。在更新数据时,会检查版本号是否发生变化。如果版本号发生变化,说明数据被其他线程修改过,当前操作需要重试。以下是一个基于版本号的乐观锁的实现示例:

import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    private int value; // 数据值
    private AtomicInteger version = new AtomicInteger(0); // 版本号

    public void updateValue(int newValue) {
        int currentVersion = version.get(); // 获取当前版本号
        while (true) {
            // 检查版本号是否发生变化
            if (version.compareAndSet(currentVersion, currentVersion + 1)) {
                // 如果版本号未发生变化,更新数据
                value = newValue;
                System.out.println("Updated value to " + newValue + " with version " + version.get());
                break;
            } else {
                // 如果版本号发生变化,重试
                currentVersion = version.get();
                System.out.println("Version changed, retrying...");
            }
        }
    }

    public static void main(String[] args) {
        OptimisticLockExample example = new OptimisticLockExample();

        // 创建多个线程更新数据
        Thread t1 = new Thread(() -> example.updateValue(10), "Thread-1");
        Thread t2 = new Thread(() -> example.updateValue(20), "Thread-2");

        t1.start();
        t2.start();
    }
}

代码说明

  • 使用AtomicInteger来实现版本号的线程安全操作。

  • 在更新数据时,通过compareAndSet方法检查版本号是否发生变化。如果版本号未发生变化,则更新数据并增加版本号;如果版本号发生变化,则重试。

  • 多个线程更新数据时,通过版本号机制避免冲突。

2. 基于时间戳的乐观锁

        基于时间戳的乐观锁通过为每个数据项添加一个时间戳字段来实现。每次修改数据时,更新时间戳。在更新数据时,会检查时间戳是否发生变化。如果时间戳发生变化,说明数据被其他线程修改过,当前操作需要重试。以下是一个基于时间戳的乐观锁的实现示例:

import java.util.concurrent.atomic.AtomicLong;

public class OptimisticLockWithTimestamp {
    private int value; // 数据值
    private AtomicLong timestamp = new AtomicLong(System.currentTimeMillis()); // 时间戳

    public void updateValue(int newValue) {
        long currentTimestamp = timestamp.get(); // 获取当前时间戳
        while (true) {
            // 检查时间戳是否发生变化
            if (timestamp.compareAndSet(currentTimestamp, System.currentTimeMillis())) {
                // 如果时间戳未发生变化,更新数据
                value = newValue;
                System.out.println("Updated value to " + newValue + " with timestamp " + timestamp.get());
                break;
            } else {
                // 如果时间戳发生变化,重试
                currentTimestamp = timestamp.get();
                System.out.println("Timestamp changed, retrying...");
            }
        }
    }

    public static void main(String[] args) {
        OptimisticLockWithTimestamp example = new OptimisticLockWithTimestamp();

        // 创建多个线程更新数据
        Thread t1 = new Thread(() -> example.updateValue(10), "Thread-1");
        Thread t2 = new Thread(() -> example.updateValue(20), "Thread-2");

        t1.start();
        t2.start();
    }
}

代码说明

  • 使用AtomicLong来实现时间戳的线程安全操作。

  • 在更新数据时,通过compareAndSet方法检查时间戳是否发生变化。如果时间戳未发生变化,则更新数据并更新时间戳;如果时间戳发生变化,则重试。

  • 多个线程更新数据时,通过时间戳机制避免冲突。

(四)乐观锁的优缺点

优点

  1. 高性能:乐观锁减少了锁的使用,降低了锁的开销,适用于读操作较多、写操作较少的场景,能够显著提高系统的性能。
  2. 资源利用率高:乐观锁允许多个线程并发访问共享资源,提高了资源的利用率。
  3. 减少锁竞争:乐观锁通过版本号或时间戳机制避免了锁的竞争,减少了线程阻塞的可能性。

缺点

  1. 实现复杂:乐观锁的实现相对复杂,需要通过版本号或时间戳等机制来检测数据是否被修改。
  2. 冲突重试机制:乐观锁在检测到冲突时需要重试,可能会导致操作失败或性能下降,尤其是在高并发写操作较多的场景下。
  3. 适用场景有限:乐观锁适用于读操作较多、写操作较少的场景,对于写操作较多的场景,其性能优势可能不明显。

四、乐观锁与悲观锁的对比

(一)锁机制

  • 悲观锁:通过加锁机制限制对共享资源的并发访问,确保在同一时间只有一个线程能够访问共享资源。

  • 乐观锁:不加锁,通过版本号或时间戳机制检测数据是否被修改,如果检测到冲突则重试。

(二)性能

  • 悲观锁:加锁和解锁操作会增加系统开销,尤其是在高并发场景下,锁的争用可能导致线程阻塞,降低系统的性能。

  • 乐观锁:减少了锁的使用,降低了锁的开销,适用于读操作较多、写操作较少的场景,能够显著提高系统的性能。

(三)适用场景

  • 悲观锁:适用于写操作较多、数据竞争激烈的场景,例如数据库事务中的行锁和表锁。

  • 乐观锁:适用于读操作较多、写操作较少的场景,例如缓存系统、分布式系统中的数据一致性控制。


五、总结

乐观锁悲观锁
核心思想假设冲突较少,先操作再检查冲突,通过版本号或时间戳检测数据是否被修改。假设冲突较多,通过加锁机制限制对共享资源的并发访问。
锁机制不加锁,通过版本号或时间戳检测数据是否被修改。加锁,通过锁机制限制对共享资源的并发访问。
性能读操作多、写操作少时性能高,减少锁的开销。写操作多时性能可能受限,锁的争用可能导致线程阻塞。
资源利用率允许多个线程并发访问,资源利用率高。同一时间只有一个线程能访问资源,资源利用率低。
实现复杂度实现相对复杂,需要版本号或时间戳机制。实现相对简单,直接通过锁机制实现。
适用场景读操作多、写操作少的场景,如缓存系统、分布式系统中的数据一致性控制。写操作多、数据竞争激烈的场景,如数据库事务中的行锁和表锁。
冲突处理发现冲突时重试操作。通过锁机制避免冲突,其他线程等待锁释放。
数据一致性数据一致性依赖于重试机制,可能需要多次尝试。数据一致性高,通过锁机制严格保证。
并发能力并发能力强,允许多个线程同时读取。并发能力弱,同一时间只有一个线程能操作。
适用语言/框架Java中可通过Atomic类实现版本号机制;数据库中可通过版本号字段实现。Java中可通过synchronizedReentrantLock实现;数据库中可通过FOR UPDATE实现。
优点性能高、资源利用率高、减少锁竞争。数据一致性高、实现简单、安全性高。
缺点实现复杂、冲突时需要重试、适用场景有限。性能瓶颈、资源利用率低、锁竞争可能导致线程阻塞。

        乐观锁和悲观锁是两种常见的锁机制,它们在设计理念、实现方式和适用场景上各有特点。悲观锁通过加锁机制严格限制对共享资源的并发访问,能够确保数据的一致性,但可能会导致性能瓶颈。乐观锁通过版本号或时间戳机制检测数据是否被修改,减少了锁的使用,提高了系统的性能,但实现相对复杂,且在高并发写操作较多的场景下可能不适用。

        在实际应用中,选择乐观锁还是悲观锁需要根据具体的业务场景和性能需求来决定。对于写操作较多、数据竞争激烈的场景,悲观锁可能是更好的选择;而对于读操作较多、写操作较少的场景,乐观锁则能够显著提高系统的性能。

通过本文的介绍,读者可以更好地理解乐观锁和悲观锁的原理、实现和应用,从而在实际开发中合理选择锁机制,优化系统的性能和可靠性。

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

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

相关文章

在Windows下编译出llama_cpp_python的DLL后,在虚拟环境中使用方法

定位编译生成的文件 在VS2022编译完成后,在构建目录(如build/Release或build/Debug)中寻找以下关键文件: ggml.dll、ggml_base.dll、ggml_cpu.dll、ggml_cuda.dll、llama.dll(核心动态链接库) llama_cp…

关于datetime获取时间的问题

import datetime print(datetime.now())如果用上述代码,会报错: 以下才是正确代码: from datetime import datetime print(datetime.now()) 结果: 如果想格式化时间,使用代码: from datetime import da…

Unity版本使用情况统计(更新至2025年5月)

UWA发布|本期UWA发布的内容是Unity版本使用统计(第十六期),统计周期为2024年11月至2025年5月,数据来源于UWA网站(www.uwa4d.com)性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势作为参…

GPUCUDA 发展编年史:从 3D 渲染到 AI 大模型时代(上)

目录 文章目录 目录1960s~1999:GPU 的诞生:光栅化(Rasterization)3D 渲染算法的硬件化实现之路 学术界算法研究历程工业界产品研发历程光栅化技术原理光栅化技术的软件实现:OpenGL 3D 渲染管线设计 1. 顶点处理&…

人机融合智能 | 可穿戴计算设备的多模态交互

可穿戴计算设备可以对人体以及周围环境进行连续感知和计算,为用户提供随时随地的智能交互服务。本章主要介绍人机智能交互领域中可穿戴计算设备的多模态交互,阐述以人为中心的智能穿戴交互设计目标和原则,为可穿戴技术和智能穿戴交互技术的设计提供指导,进而简述支持智能穿戴交…

Impromptu VLA:用于驾驶视觉-语言-动作模型的开放权重和开放数据

25年5月来自清华和博世的论文“Impromptu VLA: Open Weights and Open Data for Driving Vision-Language-Action Models”。 用于自动驾驶的“视觉-语言-动作” (VLA) 模型前景光明,但在非结构化极端场景下却表现不佳,这主要是由于缺乏有针对性的基准测…

AI智能体,为美业后端供应链注入“智慧因子”(4/6)

摘要:本文深入剖析美业后端供应链现状,其产品具有多样性、更新换代快等特点,原料供应和生产环节也面临诸多挑战。AI 智能体的登场为美业后端供应链带来变革,包括精准需求预测、智能化库存管理、优化生产计划排程、升级供应商管理等…

跨平台资源下载工具:res-downloader 的使用体验

一款基于 Go Wails 的跨平台资源下载工具,简洁易用,支持多种资源嗅探与下载。res-downloader 一款开源免费的下载软件(开源无毒、放心使用)!支持Win10、Win11、Mac系统.支持视频、音频、图片、m3u8等网络资源下载.支持视频号、小程序、抖音、…

数据湖是什么?数据湖和数据仓库的区别是什么?

目录 一、数据湖是什么 (一)数据湖的定义 (二)数据湖的特点 二、数据仓库是什么 (一)数据仓库的定义 (二)数据仓库的特点 三、数据湖和数据仓库的区别 (一&#…

【深度学习新浪潮】如何入门三维重建?

入门三维重建算法技术需要结合数学基础、计算机视觉理论、编程实践和项目经验,以下是系统的学习路径和建议: 一、基础知识储备 1. 数学基础 线性代数:矩阵运算、向量空间、特征分解(用于相机矩阵、变换矩阵推导)。几何基础:三维几何(点、线、面的表示)、射影几何(单…

Codeforces Round 1025 (Div. 2) B. Slice to Survive

Codeforces Round 1025 (Div. 2) B. Slice to Survive 题目 Duelists Mouf and Fouad enter the arena, which is an n m n \times m nm grid! Fouad’s monster starts at cell ( a , b ) (a, b) (a,b), where rows are numbered 1 1 1 to n n n and columns 1 1 1 t…

ubuntu中使用docker

上一篇我已经下载了一个ubuntu:20.04的镜像; 1. 查看所有镜像 sudo docker images 2. 基于本地存在的ubuntu:20.04镜像创建一个容器,容器的名为cppubuntu-1。创建的时候就会启动容器。 sudo docker run -itd --name cppubuntu-1 ubuntu:20.04 结果出…

[ElasticSearch] DSL查询

🌸个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 🏵️热门专栏: 🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 🍕 Collection与…

iview中的table组件点击一行中的任意一点选中本行

<Table border ref"selection" size"small" on-row-click"onClickRow"></Table>// table组件点击一行任意位置选中onClickRow(row, index) {this.$refs.selection.toggleSelect(index)}写上toggleSelect(index)方法即可&#xff0c;…

《探秘跨网段局域网IP广播:解锁网络通信的新姿势》

一、从基础出发:广播与跨网段 在计算机网络的世界中,广播域是一个至关重要的概念。简单来说,广播域是指网络中能接收任一台主机发出的广播帧的所有主机集合。当一台主机在广播域内发出一个广播帧时,同一广播域内的所有其他主机都可以收到该广播帧。在没有路由器或 VLAN 分割…

maven微服务${revision}依赖打包无法识别

1、场景描述 我现在又一个微服务项目&#xff0c;父pom的版本&#xff0c;使用<properties>定义好&#xff0c;如下所示&#xff1a; <name>ypsx-finance-center</name> <artifactId>ypsx-finance</artifactId> <packaging>pom</pack…

2025年06月07日Github流行趋势

项目名称&#xff1a;netbird 项目地址url&#xff1a;https://github.com/netbirdio/netbird项目语言&#xff1a;Go历史star数&#xff1a;14824今日star数&#xff1a;320项目维护者&#xff1a;mlsmaycon, braginini, pascal-fischer, lixmal, pappz项目简介&#xff1a;使…

WPS中将在线链接转为图片

WPS中将在线链接转为图片 文章目录 WPS中将在线链接转为图片一&#xff1a;解决方案1、下载图片&#xff0c;精确匹配&#xff08;会员功能&#xff09;2、将在线链接直接转为图片 一&#xff1a;解决方案 1、下载图片&#xff0c;精确匹配&#xff08;会员功能&#xff09; …

实战二:开发网页端界面完成黑白视频转为彩色视频

​一、需求描述 设计一个简单的视频上色应用&#xff0c;用户可以通过网页界面上传黑白视频&#xff0c;系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观&#xff0c;不需要了解技术细节。 效果图 ​二、实现思路 总体思路&#xff1a; 用户通过Gradio界面上…

vue生成二维码图片+文字说明

需求&#xff1a;点击下载图片&#xff0c;上方是二维码&#xff0c;下方显示该二维码的相关内容&#xff0c;并且居中显示&#xff0c;支持换行 解决方案步骤&#xff1a; 1. 使用qrcode生成二维码的DataURL。 2. 创建canvas&#xff0c;将二维码图片绘制到canvas的上半部分…