并发安全之锁机制一

锁机制一

锁机制是计算机系统中解决并发冲突的核心工具,其存在和应用场景源于一个根本问题:当多个执行单元(线程、进程、分布式节点)同时访问或修改同一份共享资源时,如何保证数据的正确性、一致性和系统可靠性?

一、为什么需要锁?

想象以下场景,如果没有锁会发生什么:

  1. 银行存款取款(数据竞争):
    • 线程A读取账户余额:100元。
    • 线程B读取账户余额:100元。
    • 线程A存入50元,计算新余额 100 + 50 = 150,写入150。
    • 线程B取出30元,计算新余额 100 - 30 = 70,写入70。
    • 结果: 最终余额是70元,而不是正确的120元 (100+50-30)。线程B的操作覆盖了线程A的操作,因为两者都基于旧的余额100元进行计算。这破坏了数据一致性。
  2. 订票系统(超卖问题):
    • 剩余票数:1张。
    • 用户A和用户B同时点击购买。
    • 服务器进程A读取票数:1。
    • 服务器进程B读取票数:1。
    • 进程A判断有票,执行扣减:1-1=0,更新为0,出票成功。
    • 进程B判断有票,执行扣减:1-1=0,更新为0,出票成功。
    • 结果: 1张票卖给了两个人。这破坏了业务规则。
  3. 链表插入操作(数据结构损坏):
    • 链表: Node1 -> Node2。
    • 线程A要在Node1和Node2之间插入NodeX。
    • 线程B要删除Node2。
    • 如果没有锁控制时序,可能导致:
      • 线程A刚让Node1指向NodeX(但NodeX还没指向Node2)。
      • 线程B删除Node2,让Node1(或前一个节点)指向Node2的下一个节点(可能是NULL)。
      • 结果:NodeX悬空或链表断裂。这破坏了数据结构完整性。

这些问题的根源在于:

  • 非原子操作: 像“读取-修改-写入”这样的复合操作,如果中间被其他操作打断,就会导致结果错误。
  • 操作交错的不可预测性: 多个操作以不同的顺序和时机交织执行(交错),会产生各种预期之外的结果。
  • 内存/缓存可见性问题: 一个线程对共享变量的修改可能不会立即被其他线程看到(由于CPU缓存的存在)。

锁就是解决这些问题的“协调员”:

  1. 实现互斥: 锁确保在同一时刻,只有一个执行单元能进入受保护的代码区域(临界区)访问或修改特定的共享资源。其他执行单元必须等待锁释放。
  2. 保证原子性: 对于复合操作(如余额增减、票数扣减、链表节点指针修改),锁可以将它们包装成一个不可分割的操作单元,在执行过程中不会被其他操作打断。
  3. 保障可见性: 在释放锁时,通常会强制将修改刷新到主内存;在获取锁时,通常会强制从主内存重新加载最新值。这确保了临界区内修改的结果对后续获得锁的执行单元是可见的。
  4. 维持顺序: 锁隐式地建立了操作的先后顺序,避免了破坏性交错的产生。

二、锁有哪些应用场景?

锁的应用极其广泛,存在于计算机系统的各个层面:

  1. 操作系统内核:
    • 保护内核数据结构: 进程表、文件描述符表、内存管理结构(如页表、空闲列表)、设备驱动状态等。多个CPU核心上的线程或中断处理程序都需要安全地访问这些全局结构。
    • 同步原语实现: 信号量、条件变量、屏障等同步机制本身就需要锁(通常是自旋锁)来保护其内部状态。
    • 设备访问: 确保同一时间只有一个进程/线程能向特定硬件设备(如打印机、特定端口)发送命令或数据。
  2. 多线程应用程序:
    • 保护共享内存变量: 计数器、标志位、配置参数等。
    • 保护复杂数据结构: 链表、哈希表、树、队列等。插入、删除、查找(如果查找可能触发修改)都需要锁来防止结构损坏。
    • 单例模式实现: 确保在多线程环境下,一个类只有一个实例被创建(通常使用互斥锁+双重检查锁定)。
    • 线程池任务队列: 多个工作线程从任务队列取任务,生产者线程向队列添加任务。队列本身需要锁保护。
    • 资源池管理: 如数据库连接池、线程池。分配和回收资源需要互斥操作。
    • 缓存同步: 更新和读取共享缓存数据时。
  3. 数据库管理系统:
    • 事务并发控制: 这是数据库锁最核心的应用。数据库使用各种粒度的锁(行锁、页锁、表锁、意向锁)和不同模式的锁(共享锁/S锁、排他锁/X锁)来实现不同的事务隔离级别,保证ACID特性(特别是隔离性I和一致性C)。
      • 行锁/记录锁: 防止两个事务同时修改同一条记录。
      • 间隙锁: 防止幻读(在范围查询中插入新记录)。
      • 表锁: 在特定操作(如ALTER TABLE)或行锁冲突升级时使用。
      • 死锁检测与处理: 数据库有专门的机制来处理事务间因循环等待锁而导致的死锁。
    • 索引维护: 对B+树等索引结构进行分裂、合并等操作时需要锁保护。
    • 缓存管理: 数据库缓冲池(Buffer Pool)的管理也需要锁机制。
  4. 文件系统:
    • 文件写入: 防止多个进程同时写入同一个文件导致内容混乱。通常使用文件锁(如flock, fcntl)。
    • 元数据更新: 修改文件的属性(如大小、权限、时间戳)、目录结构(创建、删除、重命名文件/目录)时需要锁保护,避免元数据不一致。
  5. 分布式系统:
    • 分布式锁: 在多个独立的服务器或进程之间协调对共享资源的访问。例如:
      • 防止多个节点同时执行同一个定时任务。
      • 确保在分布式环境中对某个全局配置项的修改是互斥的。
      • 实现分布式环境下的选主(Leader Election)。
      • 控制对共享存储(如分布式文件系统中的一个文件)的并发写入。
      • 常用实现: ZooKeeper, Redis (RedLock), etcd, Consul 等提供的分布式锁服务。这比单机锁复杂得多,需要处理网络延迟、节点故障、时钟漂移等问题。

三、常见的锁类型

  1. 互斥锁:

    • 特点: 最基本的锁类型。严格互斥,一次只允许一个持有者。
    • 行为: 如果一个线程试图获取已被持有的互斥锁,它将被阻塞(进入睡眠状态),直到锁被释放。操作系统会进行线程调度。
    • 用途: 保护需要绝对互斥访问的临界区。
    • 例子: pthread_mutex_t (POSIX), std::mutex (C++), synchronized 关键字修饰的方法或代码块 (Java 内部使用互斥锁), Lock (数据库中的排他锁)。
  2. 自旋锁:

    • 特点: 当尝试获取锁失败时,线程不会立即阻塞,而是在一个循环中不断检查锁的状态(“自旋”)。
    • 优点: 避免上下文切换的开销,对于预期等待时间非常短的场景效率高。
    • 缺点: 浪费CPU周期(忙等待),如果等待时间长,效率极低。
    • 用途: 多处理器系统,临界区非常小且执行时间极短,且持有锁的线程不太可能被抢占的场景(如内核中断处理)。
    • 例子: pthread_spinlock_t (POSIX), std::atomic_flag (C++ 可用于实现自旋锁), 底层硬件指令如 test-and-set, compare-and-swap
  3. 读写锁:

    • 特点: 区分读操作和写操作。
      • 读锁: 允许多个读者同时持有。只要没有写者,读者就可以并发访问。
      • 写锁: 是排他的。一个写锁被持有时,不能有其他读者或写者。获取写锁通常需要等待所有现有的读者释放读锁。
    • 优点: 大幅提高读多写少场景的并发性能。
    • 缺点: 实现比互斥锁复杂;如果写操作频繁,可能导致读者或写者“饿死”(需要公平策略)。
    • 用途: 保护经常被读取但较少被修改的数据结构(如配置信息、缓存)。
    • 例子: pthread_rwlock_t (POSIX), std::shared_mutex (C++17), ReentrantReadWriteLock (Java), LOCK IN SHARE MODE / FOR SHARE (数据库共享锁), FOR UPDATE (数据库排他锁)。
  4. 悲观锁 vs 乐观锁

    类型机制适用场景
    悲观锁默认资源会被修改,访问前强制加锁(如行锁、表锁)写操作频繁的高冲突场景‌
    乐观锁通过版本号或CAS算法检测冲突,提交时校验读多写少的低冲突场景(如电商库存)‌

CAS(Compare-And-Swap) 是一种关键的无锁(Lock-Free)编程原子操作,也是实现乐观并发控制的核心。它直接由大多数现代CPU提供硬件支持(通过特定的机器指令),用于在多线程/多处理器环境下安全地更新共享变量,而无需使用传统的互斥锁。

工作流程(从线程视角)

  1. 读取: 线程读取共享变量 V 的当前值,记为 current_value
  2. 计算: 线程基于 current_value 计算出希望更新的新值 new_value
  3. CAS尝试: 线程执行 CAS(V, current_value, new_value)
    • 成功: 如果 V 的当前值仍然等于 current_value(意味着在此期间没有其他线程修改过 V),则 V 被原子地设置为 new_value,返回 true。线程的更新操作完成。
    • 失败: 如果 V 的当前值不等于 current_value(意味着在此期间有其他线程抢先修改了 V),则 CAS 什么也不做(不更新 V),返回 false
  4. 失败处理: 如果 CAS 失败,线程通常不会阻塞,而是选择:
    • 放弃: 如果操作允许失败。
    • 重试(自旋): 最常见的方式。线程回到步骤1,重新读取 V最新值作为新的 current_value,重新计算 new_value,然后再次尝试 CAS。这个循环(读取 -> 计算 -> CAS)会一直持续,直到 CAS 成功或达到某种退出条件(如重试次数上限)。

四、golang中的锁机制

在Go语言中,处理并发问题时通常优先考虑使用信道(channel),但在某些情况下,当信道无法解决问题时,就需要使用锁机制来处理共享内存的并发访问。Go语言提供了两种主要的锁类型:互斥锁(Mutex)和读写锁(RWMutex)。

1. 互斥锁(sync.Mutex)
  • 作用:确保同一时间只有一个goroutine访问共享资源。
  • 方法
    • Lock():获取锁(若锁被占用,则阻塞当前goroutine)
    • Unlock():释放锁
  • 特点
    • 不可重入:同一goroutine重复加锁会导致死锁。
    • 零值可用:未初始化的Mutex可直接使用。
var mu sync.Mutex
var counter intfunc increment() {mu.Lock()          // 加锁defer mu.Unlock()  // 确保解锁(推荐用defer避免忘记解锁)counter++
}
2. 读写锁(sync.RWMutex)
  • 适用场景:读多写少(如缓存系统)。
  • 方法
    • RLock():获取读锁(允许多个读并发)
    • RUnlock():释放读锁
    • Lock():获取写锁(独占,与读/写互斥)
    • Unlock():释放写锁
  • 规则
    • 写锁优先级高于读锁(防止读锁饿死写锁)
    • 持有读锁时无法升级为写锁
var rwMu sync.RWMutex
var data map[string]stringfunc read(key string) string {rwMu.RLock()         // 读锁defer rwMu.RUnlock()return data[key]
}func write(key, value string) {rwMu.Lock()          // 写锁defer rwMu.Unlock()data[key] = value
}
3. Mutex的优化机制
  • 自旋尝试
    当锁被短期持有时,等待的goroutine会在用户态自旋尝试(约4次),避免立即进入休眠(减少上下文切换开销)。
  • 饥饿模式
    若某个goroutine等待超过1ms,锁会进入饥饿模式——新来的goroutine直接排队(不抢锁),确保公平性。
4. RWMutex的设计
  • 读锁计数
    通过readerCount跟踪当前读锁数量(正数表示读锁,负数表示有写锁等待)。
  • 写锁优先
    当有写锁等待时,新来的读锁会被阻塞,防止写锁被饿死。

五、mysql的锁机制

1. 全局锁(Global Lock)
  • 作用:锁定整个数据库实例,使所有表处于只读状态。
  • 命令FLUSH TABLES WITH READ LOCK(FTWRL)25。
  • 场景:全库逻辑备份(如mysqldump)时确保数据一致性。
  • 风险:阻塞所有写操作,长时间锁定会导致业务瘫痪。推荐事务引擎使用–single-transaction参数(基于MVCC备份,不阻塞写)24。
2. 表级锁(Table Lock)
  • 类型
    • 普通表锁:通过LOCK TABLES ... READ/WRITE手动加锁,读锁允许多会话并发读但阻塞写,写锁独占36。
    • 元数据锁(MDL):自动加锁,保护表结构。DML操作(如SELECT)加MDL读锁,DDL操作(如ALTER TABLE)加MDL写锁,读写互斥。长事务会阻塞DDL,导致雪崩24。
    • 意向锁(Intention Lock):表级锁,分为意向共享锁(IS)和意向排他锁(IX),用于快速判断表中是否有行锁冲突48。
  • 适用引擎:MyISAM默认表锁;InnoDB显式支持。
  • 特点:开销小、加锁快、无死锁,但并发度低39。
3. 行级锁(Row Lock)
  • 适用引擎:仅InnoDB支持,细粒度锁定单行数据。
  • 特点:开销大、加锁慢、可能出现死锁,但并发度高16。
  • 加锁条件:必须通过索引检索数据,否则退化为表锁310。
4.共享锁(S锁)

共享锁(S锁)SELECT ... LOCK IN SHARE MODE,允许多事务读,阻塞写。

允许多事务‌并发读取同一数据‌,但阻止任何事务获取排他锁进行修改‌

锁兼容性:多个S锁可共存,但S锁与X锁互斥‌

5.排他锁(X锁)

排他锁(X锁)SELECT ... FOR UPDATE或自动加锁(如UPDATE),独占数据

事务持有X锁时,‌禁止其他事务加任何锁‌(包括S锁和X锁),实现独占读写‌

自动应用于写操作:UPDATEDELETEINSERT语句默认加X锁‌

6.悲观锁(Pessimistic Lock)
  1. 实现机制
    • 基于‌数据库原生锁机制‌(S锁/X锁),操作前先加锁,假设高并发冲突概率‌。
    • 典型语句:SELECT ... FOR UPDATE(X锁)、SELECT ... LOCK IN SHARE MODE(S锁)‌。
  2. 适用场景
    • 写密集型操作(如库存扣减、支付交易)‌。
    • 需强一致性的金融系统,容忍锁开销换取安全性‌。
7.乐观锁(Optimistic Lock)
  1. 实现机制

    • 无锁设计‌:通过业务层逻辑(版本号/时间戳)检测冲突,提交时校验数据一致性‌89。

    • 伪代码逻辑:

      sql CodeUPDATE products 
      SET stock = new_stock, version = version + 1 
      WHERE id = 10 AND version = current_version; -- 校验版本号
      
  2. 适用场景

    • 读多写少场景(如评论计数更新)‌。
    • 分布式系统,减少数据库锁竞争开销‌。

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

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

相关文章

结合项目阐述 设计模式:单例、工厂、观察者、代理

原文链接:https://download.csdn.net/blog/column/12433305/133862792#_1613 1、工厂模式应用 C17及之后可编译 /*日志落地模块的实现1.抽象落地基类2.派生子类(根据不同落地方向进行派生)3.使用工厂模式进行创建与表示的分离 */#ifndef _…

uniapp 更新apk有缓存点不动,卸载安装apk没有问题。android

方式一。pages.json:"globalStyle" : {"navigationBarTextStyle" : "black","navigationBarTitleText" : "uni-app","navigationBarBackgroundColor" : "#F8F8F8","backgroundColor&qu…

HTML响应式SEO公司网站源码

核心优势 100%纯HTML/CSS开发自动适配手机/平板/PC内置SEO优化结构0.5秒极速加载 包含页面 • 首页(关键词布局优化版) • 服务项目展示页 • 客户案例库 • 新闻资讯系统 • 联系方式(带地图API) 技术参数 兼容Chrome/Firefo…

Error: llama runner process has terminated: exit status 2

我是i7 12700h ,3080显卡,在 Windows 11 上运行 ollama run deepseek-r1:1.5b 出现 Error: llama runner process has terminated: exit status 2 之前是好用的,后来不知为什么就不好用了。 原因: 检查 Microsoft Visual C Redistributab…

Linux中ssh远程登录原理与配置

SSH连接的五个阶段 1. 版本协商阶段(Protocol Version Negotiation)目的:协商使用SSH-1或SSH-2协议(现代系统默认SSH-2)。流程:关键点:若版本不兼容(如客户端只支持SSH-1&#xff0c…

Kubernetes --存储入门

一、Volume 的概念对于大多数的项目而言,数据文件的存储是非常常见的需求,比如存储用户上传的头像、文件以及数据库的数据。在 Kubernetes 中,由于应用的部署具有高度的可扩展性和编排能力(不像传统架构部署在固定的位置&#xff…

蚂蚁 KAG 框架开源:知识图谱 + RAG 双引擎

引言:从RAG到KAG,专业领域知识服务的技术突破 在大语言模型(LLM)应用落地过程中,检索增强生成(RAG) 技术通过引入外部知识库有效缓解了模型幻觉问题,但在专业领域仍面临三大核心挑战…

V-Ray 7.00.08 for 3ds Max 2021-2026 安装与配置教程(含语言补丁)

本文介绍 V-Ray 7.00.08 渲染器在 3ds Max 2021-2026 各版本中的安装与使用配置步骤,适合需要进行可视化渲染工作的设计师、建筑师及相关从业者。附带语言补丁配置方式,帮助用户获得更顺畅的使用体验。 📁 一、安装文件准备 软件名称&#xf…

Go-Elasticsearch Typed Client查询请求的两种写法强类型 Request 与 Raw JSON

1 为什么需要两种写法? 在 Golang 项目中访问 Elasticsearch,一般会遇到两类需求:需求场景特点最佳写法后台服务 / 业务逻辑查询固定、字段清晰,需要编译期保障Request 结构体仪表盘 / 高级搜索 / 模板 DSL查询片段由前端或脚本动…

Leaflet 综合案例-聚类图层控制

看过的知识不等于学会。唯有用心总结、系统记录,并通过温故知新反复实践,才能真正掌握一二 作为一名摸爬滚打三年的前端开发,开源社区给了我饭碗,我也将所学的知识体系回馈给大家,助你少走弯路! OpenLayers…

React组件中的this指向问题

在 React 组件中,函数定义方式影响this指向的核心原因是箭头函数与普通函数的作用域绑定规则不同,具体差异如下:​ 1. 普通函数(function定义)需要手动bind(this)的原因​ 当用function在组件内定义方法时&#xff1…

Vue 项目中的组件引用如何实现,依赖组件间的数据功能交互及示例演示

在 Vue 项目中,组件间的引用与数据交互是核心功能之一。以下是组件引用和数据交互的详细实现方式及示例:一、组件引用方式 1. 基本组件引用 局部注册:在父组件中按需引入子组件并注册。 // ParentComponent.vue import ChildComponent from .…

✨ 使用 Flask 实现头像文件上传与加载功能

文章目录&#x1f9f1; 技术栈&#x1f5c2;️ 项目结构与配置&#x1f510; 用户身份校验逻辑&#x1f4e4; 头像上传接口&#xff1a;/file/avatar/upload&#x1f4e5; 加载头像接口&#xff1a;/file/avatar/load/<filename>&#x1f9ea; 示例请求&#xff08;使用 …

去除视频字幕 5: 使用 ProPainter, 记录探索过程

使用 ProPainter 去除视频上的字幕&#xff0c;效果演示&#xff08;比之前好多了。&#xff09;。 1. 项目目标 去除视频 (bear.webm) 中的硬字幕。 2. 初始尝试与关键失败&#xff1a;IOPaint 方法: 使用 IOPaint&#xff08;一个图像修复工具&#xff09;配合 PaddleOCR 逐…

JavaScript HTTP 请求:从老古董到新潮流

前端开发离不开跟后端打交道&#xff0c;HTTP 请求就是这座桥梁。JavaScript 提供了好几种方式来发请求&#xff0c;从老牌的 XMLHttpRequest (XHR) 到现代的 Fetch API&#xff0c;再到各种好用的第三方库&#xff08;像 Axios、Ky、Superagent&#xff09;。咱们一个一个聊清…

Windows10系统使用Cmake4.1.0构建工具+Visual Studio2022编译Opencv4.11教程

安装提示 后续安装本Cmake和Opencv版本及以上都可以。Microsoft Visual Studio2022已默认安装&#xff0c;没有安装给出教程链接。 一、Cmake4.1.0下载 1.官网下载&#xff1a;https://cmake.org/download/&#xff0c;找到cmake-4.1.0-rc3-windows-x86_64.zip版本 2.压缩包…

【性能测试】Jmeter+Grafana+InfluxDB+Prometheus Windows安装部署教程

一、工具作用与整体架构 1.1 各工具核心作用 工具作用描述关键特性Jmeter性能测试工具&#xff0c;模拟多用户并发请求&#xff0c;生成测试数据支持HTTP/HTTPS、数据库等多种协议&#xff0c;可自定义测试场景InfluxDB时序数据库&#xff0c;专门存储时间序列数据&#xff0…

【Kubernetes】使用Deployment进行的资源调度,资源清理,伸缩与更新管控

Kubernetes Deployment 实战&#xff1a;从资源清理到伸缩与更新管控 一、基础准备&#xff1a;清理闲置 ReplicaSet 在使用 Deployment 时&#xff0c;每次更新都会生成新的 ReplicaSet&#xff08;简称 RS&#xff09;&#xff0c;旧的 RS 会被保留但设置为 DESIRED0。这些闲…

stm32使用USB虚拟串口,因电脑缺少官方驱动而识别失败(全系列32单片机可用)

驱动下载地址 官网地址&#xff1a;https://www.st.com/en/development-tools/stsw-stm32102.html

枚举中间位置基础篇

参考资料来源灵神在力扣所发的题单&#xff0c;仅供分享学习笔记和记录&#xff0c;无商业用途。 核心思路&#xff1a; 一&#xff1a;直接直接用数据结构记录需要的数据&#xff0c;在枚举右&#xff0c;维护左的循环中&#xff0c;删除当前位置的元素即可达成一样效果 二…