通过前面对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

并发编程 - 线程同步(六)之锁lock的更多相关文章

  1. Python并发编程-线程同步(线程安全)

    Python并发编程-线程同步(线程安全) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 线程同步,线程间协调,通过某种技术,让一个线程访问某些数据时,其它线程不能访问这些数据,直 ...

  2. 并发编程---线程 ;python中各种锁

    一,概念 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程 线程顾名思义,就是一条流水线工作的过程,一条流水线必须属于一个车间,一个车间的工作过程是一个进程 --车间负责把资源整合到 ...

  3. 10 并发编程-(线程)-GIL全局解释器锁&死锁与递归锁

    一.GIL全局解释器锁 1.引子 在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势 首先需要明确的一点是GIL并不是Python的特性,它是在实现Pyt ...

  4. UNIX环境高级编程——线程同步之互斥锁、读写锁和条件变量(小结)

    一.使用互斥锁 1.初始化互斥量 pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量 int pthread_mutex_init( ...

  5. 33 - 并发编程-线程同步-Event-lock

    目录 1 线程同步 1.1 Event 1.1.1 什么是Flag? 1.1.2 Event原理 1.1.3 吃包子 1.2 Lock 1.2.1 lock方法 1.2.2 计数器 1.2.3 非阻塞 ...

  6. (删)Java线程同步实现二:Lock锁和Condition

    在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步.但在Java中还有一种方式可以实现线程同步,那就是Lock锁. 一.同步锁 ...

  7. java并发编程笔记(六)——AQS

    java并发编程笔记(六)--AQS 使用了Node实现FIFO(first in first out)队列,可以用于构建锁或者其他同步装置的基础框架 利用了一个int类型表示状态 使用方法是继承 子 ...

  8. C#并行编程-线程同步原语

    菜鸟学习并行编程,参考<C#并行编程高级教程.PDF>,如有错误,欢迎指正. 目录 C#并行编程-相关概念 C#并行编程-Parallel C#并行编程-Task C#并行编程-并发集合 ...

  9. Python并发编程-线程

    Python作为一种解释型语言,由于使用了全局解释锁(GIL)的原因,其代码不能同时在多核CPU上并发的运行.这也导致在Python中使用多线程编程并不能实现并发,我们得使用其他的方法在Python中 ...

  10. 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理

    (一)公平锁 1.什么是公平锁? 公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁. 2.在java中的实现? 在java的并发包中 ...

随机推荐

  1. Java线程池架构2-多线程调度器

      http://ifeve.com/java线程池架构2-多线程调度器(scheduledthreadpoolexecutor)/ 在前面介绍了java的多线程的基本原理信息:<Java线程池 ...

  2. Gitee三方登录_Python (超详细)

    第三方登录是一种常见的身份验证机制,允许用户使用他们在其他平台(如社交媒体.电子邮件服务或开发平台)的账号来登录你的应用或网站,而不需要创建新的用户名和密码.这种方式不仅简化了用户的登录过程,还提高了 ...

  3. 基于知识图谱的医疗问答系统(dockerfile+docker-compose)

    目录 一.搭建 Neo4j 图数据库 1.方式选择 2.Dockerfile+docker-compose部署neo4j容器 2.1.更新 yum 镜像源 2.2.安装 docker-ce 社区版 2 ...

  4. Python Tkinter 弹窗美化指南

    在Python编程中,Tkinter是标准GUI(图形用户界面)库,它允许开发者创建桌面应用程序.尽管Tkinter提供了基本的窗口和控件功能,但默认的样式和外观往往显得单调.因此,对Tkinter弹 ...

  5. 【Java高级编程】Java多线程学习笔记

    Java 多线程 目录 Java 多线程 1.多线程创建 方法1:通过 继承 thread 类 方法2:通过 实现 Runnable 接口 2.线程中的相关方法 (1)设置优先级 setPrlorty ...

  6. jQuery ajax - serializeArray() 方法 实例表单提交

    serializeArray()在ajax表单提交时候非常方便获取元素 定义和用法 serializeArray() 方法通过序列化表单值来创建对象数组(名称和值). 您可以选择一个或多个表单元素(比 ...

  7. Qt/C++音视频开发79-采集websocket视频流/打开ws开头的地址/音视频同步/保存到MP4文件/视频回放

    一.前言 随着音视频的爆发式的增长,各种推拉流应用场景应运而生,基本上都要求各个端都能查看实时视频流,比如PC端.手机端.网页端,在网页端用websocket来接收并解码实时视频流显示,是一个非常常规 ...

  8. Qt编写地图综合应用28-闪烁点图

    一.前言 Qt除了内置了各种UI组件以外,还直接集成了浏览器控件,注意哦这可是跨平台的浏览器控件哦,在5.6版本以前集成的是webkit,以后集成的是webengine,使得程序的灵活性拓展性大大增强 ...

  9. Qt音视频开发35-Onvif图片参数

    一.前言 视频中的图片的配置参数一般有亮度.饱和度.对比度.锐度等,以前一直以为这些需要通过厂家的私有协议SDK来设置才行,后面通过研究Onvif Device Manager 和 Onvif Dev ...

  10. 使用 Visual Studio Code 进行调试

    现在是时候实践你新获得的调试知识了. 我们刚好有一个完美的机会. 在我们的 Tailwind Traders 应用中,我们正在开发一项新功能:允许以多种货币显示产品的价格. 一位同事为这一目的编写了一 ...