常见的“锁”有哪些?

悲观锁

悲观锁认为在并发环境中,数据随时可能被其他线程修改,因此在访问数据之前会先加锁,以防止其他线程对数据进行修改。常见的悲观锁实现有:

1.互斥锁

原理:互斥锁是一种最基本的锁类型,同一时间只允许一个线程访问共享资源。当一个线程获取到互斥锁后,其他线程如果想要访问该资源,就必须等待锁被释放。

应用场景:适用于写操作频繁的场景,如数据库中的数据更新操作。在 C++ 中可以使用 std::mutex 来实现互斥锁,示例代码如下:

#include <iostream>
#include <mutex>
#include <thread>std::mutex mtx;
int sharedResource = 0;void increment() {std::lock_guard<std::mutex> lock(mtx);sharedResource++;
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Shared resource: " << sharedResource << std::endl;return 0;
}

2.读写锁

原理:读写锁允许多个线程同时进行读操作,但在进行写操作时,会独占资源,不允许其他线程进行读或写操作。读写锁分为读锁和写锁,多个线程可以同时获取读锁,但写锁是排他的。

应用场景:适用于读多写少的场景,如缓存系统。在 C++ 中可以使用 std::shared_mutex 来实现读写锁,示例代码如下: 

#include <iostream>
#include <shared_mutex>
#include <thread>std::shared_mutex rwMutex;
int sharedData = 0;void readData() {std::shared_lock<std::shared_mutex> lock(rwMutex);std::cout << "Read data: " << sharedData << std::endl;
}void writeData() {std::unique_lock<std::shared_mutex> lock(rwMutex);sharedData++;std::cout << "Write data: " << sharedData << std::endl;
}int main() {std::thread t1(readData);std::thread t2(writeData);t1.join();t2.join();return 0;
}

    乐观锁

    乐观锁是一种在多线程环境中避免阻塞的同步技术,它假设大部分操作是不会发生冲突的,因此在操作数据时不会直接加锁,而是通过检查数据是否发生了变化来决定是否提交。如果在提交数据时发现数据已被其他线程修改,则会放弃当前操作,重新读取数据并重试。

    应用场景:适用于读多写少、冲突较少的场景,如电商系统中的库存管理。

    在 C++ 中,乐观锁的实现通常依赖于版本号时间戳的机制。每个线程在操作数据时,会记录数据的版本或时间戳,操作完成后再通过比较版本号或时间戳来判断是否发生了冲突。

    下面是一个使用版本号实现乐观锁的简单示例代码:

    #include <iostream>
    #include <thread>
    #include <atomic>
    #include <chrono>// 共享数据结构
    struct SharedData {int value;            // 数据的实际值std::atomic<int> version; // 数据的版本号,用于检查是否发生了修改
    };// 线程安全的乐观锁实现
    bool optimisticLockUpdate(SharedData& data, int expectedVersion, int newValue) {// 检查数据的版本号是否与预期一致if (data.version.load() == expectedVersion) {// 进行数据更新data.value = newValue;// 增加版本号data.version.fetch_add(1, std::memory_order_relaxed);return true; // 成功提交更新}return false; // 数据版本不一致,操作失败
    }void threadFunction(SharedData& data, int threadId) {int expectedVersion = data.version.load();int newValue = threadId * 10;std::cout << "Thread " << threadId << " starting with version " << expectedVersion << "...\n";std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作// 尝试更新数据if (optimisticLockUpdate(data, expectedVersion, newValue)) {std::cout << "Thread " << threadId << " successfully updated value to " << newValue << "\n";} else {std::cout << "Thread " << threadId << " failed to update (version mismatch)\n";}
    }int main() {// 初始化共享数据,值为 0,版本号为 0SharedData data{0, 0};// 启动多个线程进行乐观锁测试std::thread t1(threadFunction, std::ref(data), 1);//std::ref(data) 将 data 包装成一个引用包装器,确保 data 在传递给函数时以引用的方式传递,而不是被复制。std::thread t2(threadFunction, std::ref(data), 2);std::thread t3(threadFunction, std::ref(data), 3);t1.join();t2.join();t3.join();std::cout << "Final value: " << data.value << ", Final version: " << data.version.load() << "\n";return 0;
    }

    原子锁

    原子锁是一种基于原子操作(如CAS、test_and_set)的锁机制。与传统的基于互斥量(如 std::mutex)的锁不同,原子锁依赖于硬件提供的原子操作,允许对共享资源的访问进行同步,且通常比传统锁更加高效。它通过原子操作保证对共享资源的独占访问,而不需要显式的线程调度。

    原子锁的适用场景:

    1.简单数据类型:原子锁最常用于锁定简单的基础数据类型,例如整数、布尔值、指针等。通过原子操作,多个线程可以安全地对这些数据进行读写,而不会发生数据竞争。

    示例:std::atomic<int>, std::atomic<bool>, std::atomic<long long>

    2.计数器、标志位:当需要在多线程中维护计数器、标志位或状态变量时,原子操作非常合适。例如,当多个线程需要递增计数器时,可以用原子操作避免使用传统的互斥锁。

    示例:使用 std::atomic<int> 来维护线程安全的计数器。

    注:原子锁通常不能锁容器类型。

    什么是原子操作?

    原子操作是指不可分割的操作,在执行过程中不会被中断或干扰。原子操作保证了操作的完整性,要么完全执行,要么完全不执行,避免了在操作过程中被线程切换打断,从而避免了数据竞争和不一致的情况。

    1.自旋锁

    什么是自旋锁?

    自旋锁是一种使用原子操作来检测锁是否可用的锁机制。自旋锁是一种忙等待的锁,当线程尝试获取锁失败时,会不断地检查锁的状态,直到成功获取锁。 

    在 C++ 中,可以使用 std::atomic_flag 结合 test_and_set 操作来实现一个简单的自旋锁:

    test_and_set 是一个原子操作,它会检查一个布尔标志的值,然后将该标志设置为 true。整个操作过程是不可分割的,即不会被其他线程的操作打断。这个布尔标志通常被用作锁,线程通过检查并设置这个标志来尝试获取锁。

    工作原理

    • 检查标志状态:线程首先检查布尔标志的当前值。
    • 设置标志为 true:如果标志当前为 false,表示锁未被占用,线程将标志设置为 true,表示成功获取到锁;如果标志当前为 true,表示锁已被其他线程占用,线程未能获取到锁。
    • 返回旧值:test_and_set 操作会返回标志的旧值。线程可以根据这个返回值判断是否成功获取到锁。如果返回 false,说明成功获取到锁;如果返回 true,则需要等待锁被释放后再次尝试获取。
    #include <iostream>
    #include <atomic>
    #include <thread>
    #include <vector>std::atomic_flag lock = ATOMIC_FLAG_INIT;// 自旋锁类
    class SpinLock {
    public:void lock() {// 持续尝试获取锁,直到成功while (lock.test_and_set(std::memory_order_acquire)) {// 自旋等待}}void unlock() {// 释放锁,将标志设置为 falselock.clear(std::memory_order_release);}
    };SpinLock spinLock;
    int sharedResource = 0;// 线程函数
    void worker() {for (int i = 0; i < 100000; ++i) {spinLock.lock();++sharedResource;spinLock.unlock();}
    }int main() {std::vector<std::thread> threads;// 创建多个线程for (int i = 0; i < 4; ++i) {threads.emplace_back(worker);}// 等待所有线程完成for (auto& thread : threads) {thread.join();}std::cout << "Shared resource value: " << sharedResource << std::endl;return 0;
    }

    自旋锁优点:

    1. 无上下文切换:自旋锁不会引起线程挂起,因此避免了上下文切换的开销。在锁竞争较轻时,自旋锁可以高效地工作。

    2. 简单高效:实现简单,且不依赖操作系统调度,适合锁竞争不严重的场景。

    自旋锁缺点:

    1. CPU资源浪费:如果锁被占用,自旋锁会不断地循环检查锁的状态,浪费 CPU 时间,尤其是在锁持有时间较长时,可能导致性能问题。

    2. 不适合锁竞争场景:当有大量线程竞争同一个锁时,自旋锁的性能将大幅下降,因为大部分时间都在自旋,浪费了 CPU 资源。

    自旋锁的适用场景:

    1. 短时间锁竞争:自旋锁适用于临界区代码执行时间非常短的情况。如果锁持有时间较长,使用自旋锁就不合适了。

    2. 锁竞争较轻:在多线程程序中,如果线程数量较少且资源竞争较少,自旋锁可以有效减少线程上下文切换,提升性能。

    3. 实时系统或高性能系统:在某些对延迟非常敏感的应用场景中,自旋锁可以通过减少上下文切换来提供更低的延迟。

    总结:自旋锁是一种简单且高效的锁机制,通过原子操作避免了线程上下文切换,适合用于短时间锁竞争和低延迟要求的场景。在锁竞争激烈或锁持有时间较长时,自旋锁的性能会受到影响,这时传统的互斥锁(如 std::mutex)可能更为合适。

    递归锁

    在 C++ 中,递归锁也被称为可重入锁,它是一种特殊的锁机制,允许同一个线程多次获取同一把锁而不会产生死锁。

    原理

    普通的互斥锁(如 std::mutex)不允许同一个线程在已经持有锁的情况下再次获取该锁,否则会导致死锁。因为当线程第一次获取锁后,锁处于被占用状态,再次尝试获取时,由于锁未被释放,线程会被阻塞,而该线程又因为被阻塞无法释放锁,从而陷入死循环。

    递归锁则不同,它内部维护了一个计数器和一个持有锁的线程标识。当一个线程第一次获取递归锁时,计数器加 1,同时记录该线程的标识。如果该线程再次请求获取同一把锁,计数器会继续加 1,而不会被阻塞。当线程释放锁时,计数器减 1,直到计数器为 0 时,锁才会真正被释放,其他线程才可以获取该锁。

    应用场景:

    • 递归调用:在递归函数中,如果需要对共享资源进行保护,使用递归锁可以避免死锁问题。例如,在一个递归遍历树结构的函数中,可能需要对树节点的某些属性进行修改,此时可以使用递归锁来保证线程安全。
    • 嵌套锁:当代码中存在多层嵌套的锁获取操作,且这些操作可能由同一个线程执行时,递归锁可以避免死锁。例如,一个函数内部调用了另一个函数,这两个函数都需要获取同一把锁。

    注意事项:

    1. 性能开销

    递归锁的实现比普通互斥锁更为复杂。普通互斥锁只需简单地标记锁的占用状态,当一个线程请求锁时,检查该状态并进行相应操作。而递归锁除了要维护锁的占用状态,还需要记录持有锁的线程标识以及一个计数器,用于跟踪同一个线程获取锁的次数。每次获取和释放锁时,都需要对这些额外信息进行更新和检查,这无疑增加了系统的开销。

    • 时间开销:由于额外的状态检查和更新操作,递归锁的加锁和解锁操作通常比普通互斥锁更耗时。在高并发、对性能要求极高的场景下,频繁使用递归锁可能会成为性能瓶颈。
    • 资源开销:记录线程标识和计数器需要额外的内存空间,虽然这部分开销相对较小,但在资源受限的系统中,也可能会产生一定的影响。

    建议:在不需要递归获取锁的场景下,应优先使用普通互斥锁(如 std::mutex)。

    2. 死锁风险

    虽然递归锁允许同一个线程多次获取同一把锁而不会死锁,但如果在递归调用过程中,锁的获取和释放逻辑出现错误,仍然可能导致死锁。例如,在递归函数中,获取锁后在某些条件下没有正确释放锁就进行了递归调用,可能会导致锁无法正常释放,其他线程请求该锁时就会陷入死锁。

    #include <iostream>
    #include <thread>
    #include <mutex>std::recursive_mutex recMutex;void faultyRecursiveFunction(int n) {if (n == 0) return;std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;if (n == 2) {// 错误:没有释放锁就返回,可能导致死锁return;}faultyRecursiveFunction(n - 1);
    }int main() {std::thread t(faultyRecursiveFunction, 3);t.join();return 0;
    }
    

    3.不同递归锁之间的交叉锁定

    当存在多个递归锁时,如果不同线程以不同的顺序获取这些锁,就可能会产生死锁。例如,线程 A 先获取了递归锁 L1,然后尝试获取递归锁 L2;而线程 B 先获取了递归锁 L2,然后尝试获取递归锁 L1。此时,两个线程都在等待对方释放锁,从而陷入死锁状态。

    在 C++ 标准库中,std::recursive_mutex 是递归锁的实现。以下是一个简单的示例代码:

    #include <iostream>
    #include <thread>
    #include <mutex>std::recursive_mutex recMutex;// 递归函数,多次获取递归锁
    void recursiveFunction(int n) {if (n == 0) return;// 加锁std::lock_guard<std::recursive_mutex> lock(recMutex);std::cout << "Recursive call: " << n << std::endl;// 递归调用recursiveFunction(n - 1);// 锁在离开作用域时自动释放
    }int main() {std::thread t(recursiveFunction, 3);t.join();return 0;
    }
    

    什么是锁的重入与不可重入?

    可重入锁也叫递归锁,允许同一个线程在已经持有该锁的情况下,再次获取同一把锁而不会产生死锁。可重入锁内部会维护一个持有锁的线程标识和一个计数器。当线程第一次获取锁时,会记录该线程的标识,并将计数器初始化为 1。如果该线程再次请求获取同一把锁,锁会检查请求线程的标识是否与当前持有锁的线程标识相同,如果相同,则将计数器加 1,而不会阻塞该线程。释放锁时,计数器减 1,直到计数器为 0 时,锁才会释放,其他线程才可以获取该锁。

    不可重入锁不允许同一个线程在已经持有该锁的情况下再次获取同一把锁。如果一个线程已经持有了不可重入锁,再次请求获取该锁时,会导致线程阻塞,进而可能产生死锁。不可重入锁只关注锁的占用状态,不记录持有锁的线程标识和获取锁的次数。当一个线程请求获取锁时,锁会检查其是否已被占用,如果已被占用,无论请求线程是否就是持有锁的线程,都会将该线程阻塞。

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

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

    相关文章

    深入理解 Python 作用域:从基础到高级应用

    在 Python 编程中&#xff0c;作用域是一个至关重要的概念&#xff0c;它决定了变量和函数的可见性与生命周期。正确理解和运用作用域规则&#xff0c;对于编写结构清晰、易于维护的代码起着关键作用。无论是简单的脚本还是复杂的大型项目&#xff0c;作用域都贯穿其中&#xf…

    ubuntu磁盘清理垃圾文件

    大头文件排查 #先查看是否是内存满了&#xff0c;USER 很高即是满了 du -f#抓大头思想&#xff0c;优先删除大文件#查看文件目录 内存占用量并排序&#xff0c;不断文件递归下去 du --max-depth1 -h /home/ -h | sort du --max-depth1 -h /home/big/ -h | sort 缓存文件清理…

    ctf网络安全题库 ctf网络安全大赛答案

    此题解仅为部分题解&#xff0c;包括&#xff1a; 【RE】&#xff1a;①Reverse_Checkin ②SimplePE ③EzGame 【Web】①f12 ②ezrunner 【Crypto】①MD5 ②password ③看我回旋踢 ④摩丝 【Misc】①爆爆爆爆 ②凯撒大帝的三个秘密 ③你才是职业选手 一、 Re ① Reverse Chec…

    VSCode集成deepseek使用介绍(Visual Studio Code)

    VSCode集成deepseek使用介绍&#xff08;Visual Studio Code&#xff09; 1. 简介 随着AI辅助编程工具的快速发展&#xff0c;VSCode作为一款轻量级、高度可扩展的代码编辑器&#xff0c;已成为开发者首选的工具之一。DeepSeek作为AI模型&#xff0c;结合Roo Code插件&#x…

    git 常用功能

    以下是 Git 的常用功能及其命令&#xff1a; 初始化仓库 git init在当前目录初始化一个新的 Git 仓库。 克隆仓库 git clone <仓库地址>将远程仓库克隆到本地。 查看状态 git status查看工作区和暂存区的状态。 添加文件到暂存区 git add <文件名>将文件添…

    Unity 脚本控制3D人物模型的BlendShape

    有些3D角色模型带有BlendShape面部控制, 在Unity中可以通过接口访问并操作其参数可以表现不同的面部表情 在Unity中选中角色模型的指定部位,这个是由模型师定义的,不固定.但肯定是在面部建模上. 点选之后在检查器可以看到对应的BlendShapes设定项出现在SkinedMeshRenderer组件…

    vscode设置终端复制快捷键(有坑!!!)

    vscode的编辑页面和终端的复制粘贴快捷键是不一样的。 vscode的终端复制快捷键为ctrlshiftC&#xff0c;当然&#xff0c;自己可以自定义设置 vscode设置终端复制快捷键&#xff08;有坑&#xff01;&#xff01;&#xff01;&#xff09;_vs code 不能复制-CSDN博客文章浏览…

    Ansible 学习笔记

    这里写自定义目录标题 基本架构文件结构安装查看版本 Ansible 配置相关文件主机清单写法 基本架构 Ansible 是基于Python实现的&#xff0c;默认使用22端口&#xff0c; 文件结构 安装 查看用什么语言写的用一下命令 查看版本 Ansible 配置相关文件 主机清单写法

    0083.基于springboot+uni-app的社区车位租赁系统小程序+论文

    一、系统说明 基于springbootuni-app的社区车位租赁系统小程序,系统功能齐全, 代码简洁易懂&#xff0c;适合小白学编程。 现如今&#xff0c;信息种类变得越来越多&#xff0c;信息的容量也变得越来越大&#xff0c;这就是信息时代的标志。近些年&#xff0c;计算机科学发展…

    NavVis VLX三维扫描:高层建筑数字化的革新力量【沪敖3D】

    在三维激光扫描领域&#xff0c;楼梯结构因其复杂的空间形态和连续垂直移动的实际需求&#xff0c;一直是技术难点之一。利用NavVis VLX穿戴式移动扫描系统成功完成一栋34层建筑的高效扫描&#xff0c;其中楼梯部分的数据一遍成形且无任何分层或形变。本文将深入分析该项目的技…

    3D模型在线转换工具:轻松实现3DM转OBJ

    3D模型在线转换是一款功能强大的在线工具&#xff0c;支持多种3D模型格式的在线预览和互转。无论是工业设计、建筑设计&#xff0c;还是数字艺术领域&#xff0c;这款工具都能满足您的需求。 3DM与OBJ格式简介 3DM格式&#xff1a;3DM是一种广泛应用于三维建模的文件格式&…

    引入elementUI时报错undefined is not an object (evaluating ‘h.a.prototype‘)

    把这两个引入方式都做了 于是报错&#xff1a; 把CDN的删掉就好了。

    PHP商协会管理系统小程序源码

    &#x1f4ca; 商协会管理系统 &#x1f4bb; 这是一款基于ThinkPHPUniapp框架&#xff0c;经过深度定制与匠心打造的商协会系统&#xff0c;被誉为商协会领域数字化运营管理的新锐之星。它以“智慧化会员体系、智敏化内容运营、智能化活动构建”为三大核心动力源&#xff0c;…

    端边云架构

    端边云架构是一种分布式计算架构&#xff0c;它将计算任务分布在终端设备、边缘节点和云端服务器之间&#xff0c;以实现高效的数据处理和资源管理。这种架构在现代物联网&#xff08;IoT&#xff09;、智能城市、工业互联网等场景中得到了广泛应用。以下是端边云架构的主要组成…

    用AI写游戏3——deepseek实现kotlin android studio greedy snake game 贪吃蛇游戏

    项目下载 https://download.csdn.net/download/AnalogElectronic/90421306 项目结构 就是通过android studio 建空项目&#xff0c;改下MainActivity.kt的内容就完事了 ctrlshiftalts 看项目结构如下 核心代码 MainActivity.kt package com.example.snakegame1// MainA…

    【数据库系统概论】数据库设计

    7.1 数据库设计概述 定义 数据库设计是指对于一个给定的应用环境&#xff0c;构造&#xff08;设计&#xff09; 优化的 数据库模式、内模式和外模式&#xff0c;并据此建立数据库及其 应用系统 &#xff0c;使之能够有效地存储和管理数据&#xff0c;满足各种用户的应用需求…

    Element UI日期选择器默认显示1970年解决方案

    目录 问题背景 问题根源 1. 数据绑定类型错误 2. 初始化逻辑错误 解决方案 核心思路 步骤 1&#xff1a;正确初始化日期对象 步骤 2&#xff1a;处理数据交互 步骤 3&#xff1a;处理年份切换事件 完整代码示例 注意事项 1. 时区问题 2. 格式化绑定值 常见问题 1. 为什…

    kafka-保姆级配置说明(producer)

    配置说明的最后一部分&#xff1b; ##指定kafka集群的列表&#xff0c;以“,”分割&#xff0c;格式&#xff1a;“host:port,host:port” ##此列表用于producer&#xff08;consumer&#xff09;初始化连接使用&#xff0c;server列表可以为kafka集群的子集 ##通过此servers列…

    .NET周刊【2月第2期 2025-02-09】

    国内文章 开箱即用的.NET MAUI组件库 V-Control 发布了! https://www.cnblogs.com/jevonsflash/p/18701494 文章介绍了V-Control&#xff0c;一个适用于.NET MAUI的组件库。作者计划将其开源&#xff0c;强调.NET MAUI是生产力强的跨平台移动开发工具。V-Control提供多种组件…

    PHP2(WEB)

    ##解题思路 打开页面什么线索都没有&#xff0c;目录扫描只是扫出来一个index.php&#xff0c;而源代码没有东西&#xff0c;且/robots.txt是不允许访问的 于是一番查询后发现&#xff0c;有个index.phps的文件路径&#xff0c;里头写着一段php的逻辑&#xff0c;对url的id参数…