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

相关推荐
geovindu20 分钟前
CSharp: Template Method Pattern
开发语言·后端·c#·.net·模板方法模式·行为模式
CSDN_RTKLIB34 分钟前
SetImpliedSelection
c#
光影少年40 分钟前
react navite封装一个RN 通用按钮组件
前端·javascript·react native·react.js·前端框架
软件黑马王子42 分钟前
1.non-MonoBehaviour 单例模式泛型基类
unity·前端框架·c#
高彬2 小时前
SAP ECC6.0 对接 C# web Services服务
开发语言·前端·c#·sap
小江的记录本2 小时前
【CSS】CSS 核心:盒模型、BFC/IFC、Flex/Grid 布局、响应式布局、移动端适配(附《思维导图》)
前端·css·面试·前端框架·tensorflow·html5·xss
我命由我123452 小时前
JS Mock(Mock 拦截请求的前提、Mock 使用、关闭 Mock)
开发语言·前端·javascript·前端框架·html·html5·js
半亩码田2 小时前
C#转Python第4.4篇:JSON 处理:json 模块 vs System.Text.Json
python·c#·json
小江的记录本2 小时前
【HTML】HTML5 新特性、语义化标签、浏览器渲染流程(附《思维导图》)
前端·css·面试·前端框架·html·css3·html5