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

相关推荐
小羊没烦恼!20 小时前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
神仙别闹20 小时前
基于C#+MySQL实现(WinForm)个人聊天室软件
c#
kybs19911 天前
全球灾害数据分析可视化 毕业设计-附源码66794
vue.js·spring boot·mysql·安全·django·c#·asp.net
Polevne1 天前
C# SFTP客户端实现文件传输
c#·ssh
samble1 天前
从零搭建灌装监控系统(四):深色主题与样式系统,ResourceDictionary 统一管理
c#·wpf·mvvm·样式·工业控制
zihan5181 天前
自己做软件日记9/21 得到了一个良好的文件结构框架
后端·flutter·前端框架·android studio
慧一居士1 天前
Nuxt 框架 插件plugins功能介绍和使用示例
前端框架
cjp5601 天前
024 . WPF MVVM类封装优化
c#
淡海水2 天前
13-04-面试-源码级深度追问链
数据结构·unity·面试·c#·游戏引擎·源码·il2cpp
asdzx672 天前
如何使用 C# 提取 PPT 文本与图片
c#·ppt·提取文本