Bootstrap

C# 基础(十四)C#单例模式:首先介绍 单线程、多线程、加锁 单例模式。然后介绍单例模式的线程同步:多线程有序访问共享内存。

一、简介

本篇文章将介绍如何使用单例模式,也就是类的实例化,在整个项目的生命周期内,只实例化一次。在单例模式中,往往可以看到如SourceCode.cs:这样的结构的。

SourceCode.cs:

public class Singleton {
    private static Singleton instance;
 
    private Singleton() {
    }
 
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

我的项目中,也有类似的结构:

好了,我把 private GlobalDataStruct()的代码显示如下:

    private GlobalDataStruct()
        {
            m_cApplicationDir = System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
            m_dbKrayConfig = new cKrayDBase();
            m_dbPatientInfo = new cKrayDBase();
            m_csStudyPatientInfo = new PatientInfo();
            m_csStudySeriesInfo = new SeriesInfo();
            m_csConfigFileInfo = new ConfigStructData();


            m_bEnableExpFlag = true;
            m_bConfigDbOpenFlag = false;
            m_bPatientDbOpenFlag = false;

            m_bHandPressStatus = false;
            m_bFootBreakStatus = false;
            m_bGenLoadStatus = false;
            m_bCanLoadStatus = false;
            m_bAlreadSelectCheckListStatus = false;
            m_bReviewCheckFlag = false;
            m_bHandCheckFlag = false;
            m_bSpliceFlag = false;
            m_bDRGenExpStatus = false;
            m_bFluGenExpStatus = false;
            m_bLoadCurCheckDeviceMode = true;

            m_sStudyBodyName = "";
            m_nFrameRate = 0;
            m_nGrabAcqStatus = 0;
            m_nSelectCheckIndex = 0;

            m_strDBDriver = "Provider = Microsoft.ACE.OLEDB.12.0; Data Source =";
            InitPublicInfo();
            InitDeviceInfo();
        }

并且,我把   public static GlobalDataStruct CreateInstance()的代码显示如下:

 public static GlobalDataStruct CreateInstance()
        {
            //双重加锁
            if (_instance == null)
            {
                lock (lockobj)//线程同步问题
                {
                    if (_instance == null)
                    {
                        _instance = new GlobalDataStruct();
                    }
                }
            }

            return _instance;
        }

好了不用多说,你会发现SourceCode.cs和我截图的代码结构,有着异曲同工之妙。并且发现,下面的几部分基本相同

private static Singleton instance

private Singleton() {  }

public static  Singleton getInstance() {}

 

 好了,下面,我先来分析这三部分的内容,然后再分析单例模式的写法。

二、单例模式的结构

主要参考(这三个链接的文章都很好):

https://www.cnblogs.com/zhili/p/SingletonPatterm.html

https://zhidao.baidu.com/question/547674677.html

https://blog.csdn.net/u012975370/article/details/62046317

单例模式的结构,往往如下:

public class Singleton {
    //类的实例化:必须是private,防止Singleton类实例化时对它进行更改。且用static保留一份。
    private static Singleton instance;
  
    //默认构造函数,但是外界不能创建该类实例
    private Singleton() {
    }
 
    //public给外部访问。且用static保留一份。
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

private static Singleton instance;——表示类的实例化,用static来实例化。

private Singleton() { }—— 一般来说,C#类的默认构造函数,都是public的。但是这里却用到了private。原因很简单,防止用户使用构造函数重新构造,确保生成的实例是单一的。比如我再次实例,就不行啦,如下图:

 public static Singleton getInstance() {} ——定义公有方法提供一个全局访问点。

         好了,单例模式的结构,我们已经基本弄明白了。接下来,看看单例模式的几种常用写法。

三、单例模式的几种写法

主要参考:

https://www.cnblogs.com/zhili/p/SingletonPatterm.html提到了单线程、多线程、加锁

https://blog.csdn.net/u012975370/article/details/62046317提到了懒汉式、饿汉式、加锁、枚举、静态内部类、容器

1、单例模式(懒汉式,),在单线程下的实现方法:

/// <summary>
    /// 单例模式的实现
    /// </summary>
    public class Singleton
    {
        // 定义一个静态变量来保存类的实例
        private static Singleton uniqueInstance;

        // 定义私有构造函数,使外界不能创建该类实例
        private Singleton()
        {
        }

        /// <summary>
        /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
        /// </summary>
        /// <returns></returns>
        public static Singleton GetInstance()
        {
            // 如果类的实例不存在则创建,否则直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new Singleton();
            }
            return uniqueInstance;
        }
    }

2、单例模式,在多线程下的实现方法:

方法1:简单的加锁

/// <summary>
    /// 单例模式的实现
    /// </summary>
    public class Singleton
    {
        // 定义一个静态变量来保存类的实例
        private static Singleton uniqueInstance;

        // 定义一个标识确保线程同步
        private static readonly object locker = new object();

        // 定义私有构造函数,使外界不能创建该类实例
        private Singleton()
        {
        }

        /// <summary>
        /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
        /// </summary>
        /// <returns></returns>
        public static Singleton GetInstance()
        {
            // 当第一个线程运行到这里时,此时会对locker对象 "加锁",
            // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
            // lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
            lock (locker)
            {
                // 如果类的实例不存在则创建,否则直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new Singleton();
                }
            }

            return uniqueInstance;
        }
    }

方法2:双重加锁方式

与方法不同的是,双重加锁效率更高,只需要添加 if (uniqueInstance == null) {}   这行即可。

/// <summary>
    /// 单例模式的实现
    /// </summary>
    public class Singleton
    {
        // 定义一个静态变量来保存类的实例
        private volatile static Singleton uniqueInstance;

        // 定义一个标识确保线程同步
        private static readonly object locker = new object();

        // 定义私有构造函数,使外界不能创建该类实例
        private Singleton()
        {
        }

        /// <summary>
        /// 定义公有方法提供一个全局访问点,同时你也可以定义公有属性来提供全局访问点
        /// </summary>
        /// <returns></returns>
        public static Singleton GetInstance()
        {
            // 当第一个线程运行到这里时,此时会对locker对象 "加锁",
            // 当第二个线程运行该方法时,首先检测到locker对象为"加锁"状态,该线程就会挂起等待第一个线程解锁
            // lock语句运行完之后(即线程运行完之后)会对该对象"解锁"
            // 双重锁定只需要一句判断就可以了
            if (uniqueInstance == null)
            {
                lock (locker)
                {
                    // 如果类的实例不存在则创建,否则直接返回
                    if (uniqueInstance == null)
                    {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }

 好了,你可能会注意到,双重加锁代码中,多了volatile 关键字,接下来分析volatile关键字。volatile关键字与共享内存有关。

四、共享内存(volatile关键字)、线程同步

线程同步:就是要保证多个线程  安全地 访问共享内存。

主要参考:

https://blog.csdn.net/u012975370/article/details/62046317

https://blog.csdn.net/lonestar555/article/details/49025169

多个线程同时访问一个变量,CLR为了效率,允许每个线程进行本地缓存,这就导致了变量的不一致性。volatile就是为了解决这个问题,volatile修饰的变量,不允许线程进行本地缓存,每个线程的读写都是直接操作在共享内存上,这就保证了变量始终具有一致性。即共享内存允许多个线程有序地读写变量,这些变量保存在共享内存上,保证了变量的一致性(比如单实例类)。

public class Singleton {
    private volatile static Singleton instance = null;
 
    public static Singleton getInstance() {
        Singleton inst = instance; // 创建临时变量
        if (inst == null) {
            synchronized (Singleton.class) {
                inst = instance;
                if (inst == null) {
                    inst = new Singleton();
                }
            }
        }
        return inst; // 返回临时变量
    }
 
    private Singleton() {}
}

五、总结

线程同步问题,虽然可以保证安全地访问共享内存,但是效率较慢。

;