5.单例模式问题3:多线程问题

一、单例模式的多线程问题

1. 什么是线程安全问题

当程序中存在多个线程,并且多个线程同时访问或修改同一份共享数据时,可能出现:

  • 数据状态不一致
  • 重复创建对象
  • 修改结果丢失
  • 集合读写异常
    这种问题通常称为多线程并发问题 或线程安全问题。

二、单例对象为什么可能被创建多次

复制代码
public static T Instance
{
    get
    {
        if (instance == null)
        {
            instance = CreateInstance();
        }

        return instance;
    }
}

假设线程 A 和线程 B 同时第一次访问,两个线程都可能通过:instance == null的判断,从而分别创建对象。这就破坏了单例的唯一性。

三、lock

C# 可以使用:lock。保证同一时刻只有一个线程进入指定代码块。

基本写法:

复制代码
private static readonly object instanceLock = new object();

lock (instanceLock)
{
    // 需要保护的代码
}

执行过程:

复制代码
线程 A 获得锁
    ↓
执行代码
    ↓
释放锁

线程 B 等待
    ↓
获得锁
    ↓
执行代码

lock 保护的是一段代码临界区,不是把整个对象"锁死"。

四、单例创建加锁

复制代码
private static readonly object instanceLock = new object();

public static T Instance
{
    get
    {
        lock (instanceLock)
        {
            if (instance == null)
            {
                instance = CreateInstance();
            }

            return instance;
        }
    }
}

这样即使多个线程同时第一次访问 Instance,也只有一个线程能够创建对象。但是每次访问 Instance 都需要进入 lock。而单例通常只有第一次访问需要创建对象,之后只是返回已有实例。

五、双重检查锁

可以先在锁外判断一次:

复制代码
public static T Instance
{
    get
    {
        if (instance == null)
        {
            lock (instanceLock)
            {
                if (instance == null)
                {
                    instance = CreateInstance();
                }
            }
        }
        return instance;
    }
}

这里出现了两次:

复制代码
if (instance == null)

所以称为:Double-Checked Locking,双重检查锁

1. 第一次判断

复制代码
if (instance == null)

作用:如果单例已经创建,直接返回。绝大多数访问不需要进入 lock。

2. 第二次判断

复制代码
lock (instanceLock)
{
    if (instance == null)
    {
        instance = CreateInstance();
    }
}

六、非 MonoBehaviour 单例的线程安全写法

结合前面私有构造函数和反射,可以把创建逻辑单独封装:

复制代码
using System;
using System.Reflection;

public class Singleton<T> where T : class
{
    private static T instance;
    private static readonly object instanceLock = new object();
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                lock (instanceLock)
                {
                    if (instance == null)
                    {
                        instance = CreateInstance();
                    }
                }
            }
            return instance;
        }
    }

    private static T CreateInstance()
    {
        ConstructorInfo constructor = typeof(T).GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            null,
            Type.EmptyTypes,
            null
        );

        if (constructor == null)
        {
            throw new InvalidOperationException(
                $"{typeof(T).Name} 必须拥有私有无参构造函数"
            );
        }
        return (T)constructor.Invoke(null);
    }
}

七、共享数据加锁

复制代码
private readonly object dataLock = new object();
public void AddData(object data)
{
    lock (dataLock)
    {
        dataList.Add(data);
    }
}

八、所有共享访问都要遵守同一套加锁规则

不能只给:AddData()加锁,却让其他地方直接访问集合。

例如:

复制代码
public void AddData(object data)
{
    lock (dataLock)
    {
        dataList.Add(data);
    }
}

如果另外一个方法:

复制代码
public void RemoveData(object data)
{
    dataList.Remove(data);
}

没有使用同一个锁,仍然不安全。

复制代码
public void RemoveData(object data)
{
    lock (dataLock)
    {
        dataList.Remove(data);
    }
}

只要操作的是同一份共享数据,就应该采用一致的同步策略。

九、MonoBehaviour 单例是否需要加锁

对于继承 MonoBehaviour 的单例,一般不要为了单例创建而加入多线程锁 。

原因:Unity 中大量 API 都要求在主线程使用

复制代码
GameObject
Transform
Instantiate()
Destroy()
AddComponent()

MonoBehaviour 本身也是由 Unity 主线程管理的。因此正常情况下都运行在 Unity 主线程。

如果整个 MonoBehaviour 单例只从主线程访问,就不存在多个线程同时执行 Awake() 创建单例的问题。

相关推荐
字节暗面4 小时前
客户端上报不可信:游戏服务端数据校验与对局重演设计
c#·游戏开发·服务端校验·反作弊
Amos_Web5 小时前
Rspack 源码解析(二十一):Cacheable 与 Storage 如何持久化编译结果
前端·rust·前端框架
Amos_Web5 小时前
Rspack 源码解析(二十二):ExternalsPlugin 如何在 Factorize 阶段换掉整个模块
前端·rust·前端框架
chingho5 小时前
GitHub上这10个C#项目,不仅实用,还怪好玩的
c#·github·.net·开源项目
半亩码田7 小时前
C#转Python第5.1篇:异步编程:asyncio vs async/await,语法相似但机制不同
开发语言·python·c#
阿华田5127 小时前
Java 类加载冲突实战:解决 is not an instance of
c#·linq
samble17 小时前
从零搭建灌装监控系统(十七):LiveCharts 实时温度曲线
c#·wpf·mvvm·modbus·工业控制
总要学点什么1 天前
第51篇:数据分析-热力图——笔刷画的是圆,落到地上却成了椭圆
3d·数据挖掘·数据分析·前端框架·gis·webgl·cesium
淡海水1 天前
02-07-原理篇-GC暂停时间的本质
开发语言·unity·c#·gc·暂停时间
小羊没烦恼!1 天前
OS X 上使用.NET开发应用程序
开发语言·前端·数据库·人工智能·c#·策略模式