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() 创建单例的问题。

相关推荐
泛凡(Linyongui)1 天前
基于 CW32L010 的智能厨卫调温旋钮方案--低成本 EC21 编码旋钮全解析
单片机·嵌入式硬件·c#
暴走约伯1 天前
【VFX工具链源码拆解 02】告别机械劳动(上):一键统改百个子粒子颜色与批量重命名
3d·unity·c#·材质
信息安全失业大专人员1 天前
每日了解一个智能体-WorkBuddy
开发语言·ai·c#·智能体
呆呆敲代码的小Y1 天前
【实战精选面试题】C# 面试题详细解答,按难易程度排序
android·java·游戏·c#·游戏开发·unity面试题·c#面试题
淡海水1 天前
09-01-内存-Unity资源生命周期全景
unity·c#·游戏引擎·.netcore·gc
椒颜皮皮虾྅1 天前
让 C# 读懂整张图:DeploySharp 驱动 PaddleOCR v4/v5/v6,打通 OCR 全流程
开发语言·人工智能·深度学习·学习·c#·ocr·cuda
昂好的day1 天前
C# 同步异步与多线程:Thread / Task / 线程池一次讲透(上篇)
开发语言·windows·c#
Highcharts.js1 天前
Highcharts 集成到 React 或 Vue 项目中时,如何高效地使用 TypeScript技巧?
vue.js·人工智能·react.js·typescript·前端框架·highcharts
吃一碗粉只付一碗钱2 天前
12-给SPA友好的401和403响应
c#·状态模式
caimouse2 天前
MonoDevelop 编译用户应用程序时调用的是哪个编译器
c#