并发编程 - 线程同步(六)之锁lock

news/2025/2/11 19:40:58/文章来源:https://www.cnblogs.com/hugogoos/p/18706702

通过前面对Interlocked类的学习,相信大家对线程同步机制有了更深的理解,今天我们将继续需要另一种同步机制——锁lock。

lock是C#语言中的关键字,是线程同步机制的一种简单的互斥锁实现方式,它可以保证在同一时刻只有一个线程能够访问被锁定的代码块。其工作原理也很简单,就是通过lock创建一个互斥锁,当一个线程获取到此互斥锁则此线程可以进入被lock保护的代码块,同时其他线程将被阻塞无法进入此代码块,直至第一个线程释放此互斥锁,其他线程才可以获取此互斥锁并进入代码块。

lock的使用也非常简单,语法如下:

lock (obj)
{//线程不安全的代码块
}

虽然lock使用起来简单方便,但是使用方式不正确也很容易产生各种奇奇怪怪的问题。

01、避免锁定this

这种使用方式会导致两个问题:

1.不可控性:lock(this)锁定的范围是整个实例,这也就意味着其他线程可以通过该实例中的其他方法访问该锁,进而形成一个实例中多个使用lock(this)的方法之前相互影响。

2.外部可见性:this表示当前实例的引用,它是公共的,因此外部代码也可以访问,这也就意味着外部代码可以通过lock(实例)访问lock(this)锁,从而使同步机制失去控制。

下面我们直接看代码:

public class LockThisExample
{public void Method1(){lock (this){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method1");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");Console.WriteLine($"开始休眠 5 秒");Console.WriteLine($"------------------------------------");Thread.Sleep(5000);}}public void Method2(){lock (this){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过lock(this)锁进入 Method2");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");}}
}
public static void LockThisRun()
{var example = new LockThisExample();var thread1 = new Thread(example.Method1);var thread2 = new Thread(example.Method2);var thread3 = new Thread(() =>{lock (example){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过lock(实例)锁进入 Method3");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");Console.WriteLine($"开始休眠 5 秒");Console.WriteLine($"------------------------------------");Thread.Sleep(5000);}});thread3.Start();thread1.Start();thread2.Start();
}

我们看看代码执行结果:

这里例子可以很好的说明lock(this)代理的问题,原本可以三个线程并发执行的三段代码,因为使用了同一个锁,导致三个线程只能顺序执行。其中Method1和Method2体现了同一实例内方法相互影响,Method3和Method1、Method2体现了因为相同实例导致实例内部方法和实例外部方法相互影响。

02、避免锁定公共对象

这种使用方式会导致两个问题:

1.全局影响:公共对象,特别是 public static 对象,很大概率会被多个类,甚至多个模块引用,因此锁定公共对象很可能导致全局范围内的同步,大大增加了死锁、竞争条件的产生的风险。

2.不可预测性:因为公共对象对全局可访问,因此如果其他模块锁定此公共对象,则当出现问题时将难以排除调试问题。

看下面代码:

public class PublicLock
{public static readonly object Lock = new object();
}
public class LockPublic1Example
{public void Method1(){lock (PublicLock.Lock){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public1");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");Console.WriteLine($"开始休眠 5 秒");Console.WriteLine($"------------------------------------");Thread.Sleep(5000);}}
}
public class LockPublic2Example
{public void Method1(){lock (PublicLock.Lock){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过 lock(公共对象) 锁进入 Public2");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");}}
}
public static void LockPublicRun()
{var example1 = new LockPublic1Example();var example2 = new LockPublic2Example();var thread1 = new Thread(example1.Method1);var thread2 = new Thread(example2.Method1);thread1.Start();thread2.Start();
}

在看看执行结果:

可以发现因为锁定了同一个公共对象,导致两个不同线程的不同实例,还是产生互相争抢锁的问题。

03、避免锁定字符串

在C#中,字符串因其不可变性和字符串池的原因,在整个程序中一个字符串一旦创建就不会更改,如果对其修改则产生新的字符串对象,而原字符串对象保持不变;同时如果创建两个相同内容的字符串,则它们共享同一个内存地址。

这就导致锁定字符串极其危险尤其危险,因为整个程序中任何给定字符串都只有一个实例,而在整个程序中只有锁定相同内容的字符串都会形成竞争条件。

public class LockString1Example
{public void Method1(){lock ("abc"){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String1");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");Console.WriteLine($"开始休眠 5 秒");Console.WriteLine($"------------------------------------");Thread.Sleep(5000);}}
}
public class LockString2Example
{public void Method1(){lock ("abc"){var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 通过 lock(字符串) 锁进入 String2");Console.WriteLine($"进入时间 {DateTime.Now:HH:mm:ss}");}}
}
public static void LockStringRun()
{var example1 = new LockString1Example();var example2 = new LockString2Example();var thread1 = new Thread(example1.Method1);var thread2 = new Thread(example2.Method1);thread1.Start();thread2.Start();
}

我们看看执行结果:

可以发现虽然在两个类中分别使用了两个字符串“abc”,但对于整个程序来说它们都指向了同一个实例,因此共用了一把锁。

04、小心锁定非readonly对象

这是因为如果锁对象为非只读对象,就可能发生某个lock代码块中修改锁对象,从而导致

锁对象变更,进而使得其他线程可以畅通无阻的进入该代码块。

如下示例:

public class LockNotReadonlyExample
{private object _lock = new object();public void Method1(){lock (_lock){_lock = new object();var threadId = Thread.CurrentThread.ManagedThreadId;Console.WriteLine($"线程 {threadId} 进入 Method1 , 时间 {DateTime.Now:HH:mm:ss}");Console.WriteLine($"------------------------------------");Thread.Sleep(5000);}}
}
public static void LockNotReadonlyRun()
{var example = new LockNotReadonlyExample();var thread1 = new Thread(example.Method1);var thread2 = new Thread(example.Method1);var thread3 = new Thread(example.Method1);thread1.Start();thread2.Start();thread3.Start();
}

再来看执行结果:

可以发现三个线程几乎同时进入,lock根本就没有起到锁的作用。

05、小心锁定静态对象

对于是否需要锁定静态对象取决于你的需求。

1.如果要在静态方法中使用lock时,则锁定的对象也必须要是静态对象。

2.如果希望类的每个实例都有独立的锁对象,则锁定非静态对象。

3.如果希望类的所有实例共享同一个锁,则锁定静态对象。

代码示例如下:

public class LockStaticExample
{//这是一个实例字段,意味着类的每个实例都会有一个独立的锁对象。//如果你希望类的每个实例有自己独立的锁来控制并发访问,这种方式更合适。private readonly object _lock1 = new object();//这是一个静态字段,意味着类的所有实例共享同一个锁对象。//如果你希望类的所有实例都共享同一个锁来同步对某个静态资源访问,这种方式更合适。private static readonly object _lock2 = new object();public void Method1(){lock (_lock1){// 临界区代码}}public void Method2(){lock (_lock2){// 临界区代码}}public static void Method3(){lock (_lock2){// 临界区代码}}
}

这是因为静态字段是所有实例共享的,其内存地址在整个程序的生命周期内是唯一的,所有实例访问同一个内存地址,因此锁定静态对象时要特别小心。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner

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

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

相关文章

解读 DeepSeek-R1 论文 - 通俗易懂版

引言:让 AI 学会"思考"的新突破 在近年来的人工智能浪潮中,大型语言模型(LLM)如 ChatGPT 已经能回答各种问题,但它们在复杂推理方面仍有不足。所谓复杂推理,比如解决奥数难题、编写复杂代码或进行多步逻辑推导,这些都相当于让 AI "动脑筋"思考多步。…

2025多校冲刺省选模拟赛10

过于困难,直接放弃2025多校冲刺省选模拟赛10\(T1\) A. 电车 \(5pts\)直接化简成质因数分解的形式,因质因数分解是唯一的,故可以只考虑下标为质数处的值交换。容易发现 \(2p_{1},2p_{2}>n\) 的质数 \(p_{1},p_{2}\) 交换后不会影响合法性,考虑进一步扩展。对着指数确定范…

DESTRUCTION OI(退役前要做的 100 件事)

【洛谷专栏】。 这几天会慢慢更新的,致我即将结束的 OI 旅程。 每一个事应该都会图文并茂展开写,也算是作为我回忆录的一部分(?这也给了我另外一个思路。 更新进度 -> 100。给自己起个 ID ✓cjh20090318/Chen_Jinhui,这两个应该是我最常用的 ID。微信:cjh20090318 QQ:…

linux命令操作以及常见环境部署

文档对应的视频来自bibi 黑马 Linux基础命令 Linux的目录结构/,根目录是最顶级的目录了 Linux只有一个顶级目录:/ 路径描述的层次关系同样适用/来表示 /home/itheima/a.txt,表示根目录下的home文件夹内有itheima文件夹,内有a.txtls命令 功能:列出文件夹信息 语法:ls [-l …

题解 [ARC127C] Binary Strings

【洛谷专栏】。 题意 给出 \(n,x\),请求出二进制下 \([1,2^n-1]\) 中字典序第 \(x\) 小的数是什么。 分析 从样例观察出每一个数的第一个字符都是 \(1\),然后画出 \(n=3\) 的树。节点即第 \(x\) 小的终止位置,边权从上到下依次表示二进制位。 顺着树从上往下,设当前节点在第…

[图形绘制/流程图] Mermaid : 开源的低代码图形绘制语言、协议及工具

概述:MermaidMermaid‌是一种基于Javascript的、开源的文本驱动图表生成工具/绘图工具,,使用类似于Markdown的低代码语法,它允许用户使用简单的文本语言来创建各种类型的图表,如流程图、时序图、甘特图和饼状图等。https://github.com/mermaid-js/mermaid https://mermaid…

SFM(Structure from Motion)总结(一)

什么是SFM? SFM(Structure from Motion)即运动结构恢复,通过给出多幅图像及其图像特征的一个稀疏对应集合,从而估计3D点的位置,这个求解过程通常涉及3D几何(结构)和摄像机姿态(运动)的同时估计。如何求解相关参数? 从图中可以得出,我们需要求解的主要内容有两个,一…

virt-manager 创建 Linux 虚拟机

上传 iso 到宿主机 ls -l /data1/iso/ total 4422912 -rw-r--r-- 1 root root 1774077952 Jan 22 08:51 ctyunos-2.0.1-210625-x86_64-dvd.iso -rw-r--r-- 1 root root 2754981888 May 7 2024 ubuntu-24.04-live-server-amd64.iso 运行 virt-manager virt-manager创建虚拟机 …

hyperf: 为项目定义全局函数

一,修改composer.json"autoload": {"psr-4": {"App\\": "app/"},"files": ["app/Functions.php"]}, 在files数组中增加我们的函数文件 二,源代码 app/Functions.php <?phpuse Hyperf\Context\ApplicationCo…

13. CMake工具的使用

一、什么是CMake工具CMake 是一个跨平台的构建系统生成器,主要用于管理和自动化软件项目的构建过程。它通过读取项目中的 CMakeLists.txt 文件来生成适用于不同编译器和操作系统的构建文件。对于大型或复杂的项目,直接编写和维护 Makefile 文件可能会变得非常复杂且容易出错,…

「PMOI-5」奇怪的方程 题解

哎哎,感觉是很典的题啊,但还是不会。 一些无脑的转化 首先转化成二维数组,原题中 \(2n\) 个方程相当于必须满足每一行和每一列的数之和是定值,已被选的数可以让这个位置的行与列的总和分别减去这个数,然后直接令它等于 \(0\),显然这是与原条件等价的。 另外我们可以发现有…