介绍
单例模式是23种设计模式最为简单的一种,属于创建型设计模式中的一种。
其中最主要就是保证一下代码特性:
- 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
设计模式是编码思想的一种应用,如果博客中涉及到对于涉及模式的将会采用C#、Java两种语言均实现一次,实质上没有太大的区别。如果有可能会另外采用Python并结合语言特性单独写一篇Python中对于设计模式的实现。
由于本人主修C#,故会由C#推演JAVA
任何代码实现当前业务后便是一个胜利,但是如何设计一个富有弹性、软件调用方便的代码结构才是体现架构师功力的点。
- 如果确认代码不会发生改变不需要过多的耦合设计模式,不需要为了Consle.Writeline(“Hello World”)或System.Out.Println(“Hello World”)专门去使用设计模式。
- 但是当需求迭代或者逐渐复杂的时候,你去重构,并使用设计模式组织当前的业务,设计后续可能扩展的代码编写分区(即代码如何继承、编写在那个类库/包下)
语言实现
CSharp
1.V1版本,正常编写代码
单例对象代码:
/// <summary>
/// 系统设置信息
/// </summary>
public class SystemConfig
{
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public SystemConfig(string strName,string strDescInfo)
{
Name = strName;
DescInfo = strDescInfo;
Thread.Sleep(1000);
}
/// <summary>
/// 系统名称
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// 系统描述
/// </summary>
public string DescInfo
{
get;
set;
}
public override string ToString()
{
return $"Name:{Name},DescInfo:{DescInfo}";
}
}
界面调用:
private string SystemName = "单例实现";
private string DescInfo = "介绍单例模式使用过程中各种细节";
public MainForm()
{
InitializeComponent();
}
/// <summary>
/// 显示系统简介信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnEasy_Click(object sender, EventArgs e)
{
SystemConfig pSystemConfig = new SystemConfig(SystemName, DescInfo);
Console.WriteLine(pSystemConfig);
}
2.V2版本,由于软件多个地方均需要使用系统设置信息,如下:
-
坏气味实现方式:
/// <summary> /// 显示系统简介信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnEasy_Click(object sender, EventArgs e) { SystemConfig pSystemConfig = new SystemConfig(SystemName, DescInfo); Console.WriteLine(pSystemConfig); } /// <summary> /// 其他业务:显示系统简介信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnEasy1_Click(object sender, EventArgs e) { SystemConfig pSystemConfig = new SystemConfig(SystemName, DescInfo); Console.WriteLine(pSystemConfig); }
代码中按照界面需要使用SystemConfig对象,何处实现便何处实例并调用,会出现如下不和谐的问题:
- SystemConfig需要统一添加参数,需要扩展参数时。
- 回到最原始SystemConfig配置类中,为了模拟实例过程中与数据库或配置文件交互中可能造成的效率损失,增加了一个Thread.Sleep(1000)等待1000秒。如果多出实例次数过多的话,如:需要循环调用1000次时我便需要总共等待Thread.Sleep(1000*1000)合计1000秒。
在这个场景下,实质上便是将SystemConfig对象实例随着业务的变化不断升级SystemConfig对象的级别的过程
3.V3版本,升级对象作用域,如下:
-
如果只是此类使用则再此类中增加全局的SystemConfig对象如:
public partial class MainForm : Form { private string SystemName = "单例实现"; private string DescInfo = "介绍单例模式使用过程中各种细节"; SystemConfig CurrentSystemConfig = null; public MainForm() { InitializeComponent(); CurrentSystemConfig = new SystemConfig(SystemName, DescInfo); } /// <summary> /// 显示系统简介信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnEasy_Click(object sender, EventArgs e) { Console.WriteLine(CurrentSystemConfig); } /// <summary> /// 其他业务:显示系统简介信息 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnEasy1_Click(object sender, EventArgs e) { Console.WriteLine(CurrentSystemConfig); } }
基于V3版本便已经是在一定程度上使用的了单例模式,
- 但作用域我们大胆一点将其提取到代码中可以一定知道的地方。
代码如下:
SystemConfig部分:
/// <summary>
/// 系统设置信息
/// </summary>
public class SystemConfig
{
private static string SystemName = "单例实现";
private static string SystemDescInfo = "介绍单例模式使用过程中各种细节";
private static SystemConfig m_CurrentSystemConfig = null;
public static SystemConfig CurrentSystemConfig
{
get
{
if (m_CurrentSystemConfig == null)
{
m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo);
}
return m_CurrentSystemConfig;
}
}
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public SystemConfig(string strName,string strDescInfo)
{
Name = strName;
DescInfo = strDescInfo;
Thread.Sleep(1000);
}
/// <summary>
/// 系统名称
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// 系统描述
/// </summary>
public string DescInfo
{
get;
set;
}
public override string ToString()
{
return $"Name:{Name},DescInfo:{DescInfo}";
}
}
界面调用:
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
/// <summary>
/// 显示系统简介信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnEasy_Click(object sender, EventArgs e)
{
Console.WriteLine(SystemConfig.CurrentSystemConfig);
}
/// <summary>
/// 其他业务:显示系统简介信息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnEasy1_Click(object sender, EventArgs e)
{
Console.WriteLine(SystemConfig.CurrentSystemConfig);
}
}
4.懒汉单例与饿汉单例模式的区别
两种单例模式先讲述饿汉单例模式再将懒汉单例
-
饿汉单例模式是不论你是否需要使用,我都需要将单例实例化;
-
懒汉单例顾名思义便是很懒,而在代码中的体现便是按需,即我使用时才用
- 两者的区别便是对于单例的初始化的动作的地方。饿汉便是我不论你是否使用我,我均将对象实例出来,而懒汉是你使用单例实例时我才实例化我所需的对象。
以下代码均采用部分代码,其他部分均为相同,如需向上文中获取拼接
以下为饿汉单例模式的部分代码:
-
使用代码调用的方式实现饿汉单例模式
/// <summary> /// 系统设置信息 /// </summary> public class SystemConfig { private static string SystemName = "单例实现"; private static string SystemDescInfo = "介绍单例模式使用过程中各种细节"; private static SystemConfig m_CurrentSystemConfig = null; public static SystemConfig CurrentSystemConfig { get { return m_CurrentSystemConfig; } set { m_CurrentSystemConfig = value; } } public static void CreateSingleton() { CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo); } }
在此段代码中使用代码调用CreateSingleton的方式加载饿汉单例,方便其他环节使用。
-
使用语言优势实现饿汉单例模式
///
/// 系统设置信息
///
public class SystemConfig
{
private static string SystemName = “单例实现”;
private static string SystemDescInfo = “介绍单例模式使用过程中各种细节”;private static SystemConfig m_CurrentSystemConfig = null; public static SystemConfig CurrentSystemConfig { get { return m_CurrentSystemConfig; } set { m_CurrentSystemConfig = value; } } static SystemConfig() { CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo); }
}
在此段代码中使用语言上提供的遍历使用静态构造函数static SystemConfig的遍历,实现只要使用改类时便将单例实例创建。
以下为懒汉单例模式的部分代码,其为V3版本中的部分代码
/// <summary>
/// 系统设置信息
/// </summary>
public class SystemConfig
{
private static string SystemName = "单例实现";
private static string SystemDescInfo = "介绍单例模式使用过程中各种细节";
private static SystemConfig m_CurrentSystemConfig = null;
public static SystemConfig CurrentSystemConfig
{
get
{
if (m_CurrentSystemConfig == null)
{
m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo);
}
return m_CurrentSystemConfig;
}
}
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public SystemConfig(string strName,string strDescInfo)
{
Name = strName;
DescInfo = strDescInfo;
Thread.Sleep(1000);
}
}
该代码为再使用SystemConfig.CurrentSystemConfig对象时才会构建对象。
两种单例模式(饿汉单例模式、懒汉单例模式均存在一定程度上的不足)
- 饿汉单例模式的不足为:代码需要在需要使用的地方之前,调用创建单例的函数,创建出饿汉单例,一方面代码不优雅、可读性不强,另一方面会降低应用程序的效率[特殊场景下]
- 懒汉单例模式会出现线程安全,如果多个线程同时使用该单例,在一定场景下将会出现多个线程使用的不是同一个单例。
5.V4版本使用懒汉模式,加上双重判断锁保证对象的线程安全。
- 1.首先描述懒汉单例模式会出现的线程安全是一个什么场景以及为什么会出现这样的一个问题。
当程序接入多线程时,多个线程同时访问单例时会出现,先运行的部分线程使用的单例不是同一个对象。如我们创建如下场景:
SystemConfig懒汉单例实现方式:
/// <summary>
/// 系统设置信息
/// </summary>
public class SystemConfig
{
private static string SystemName = "单例实现";
private static string SystemDescInfo = "介绍单例模式使用过程中各种细节";
private static SystemConfig m_CurrentSystemConfig = null;
public static SystemConfig CurrentSystemConfig
{
get
{
if (m_CurrentSystemConfig == null)
{
m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo);
}
return m_CurrentSystemConfig;
}
set
{
m_CurrentSystemConfig = value;
}
}
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public SystemConfig(string strName,string strDescInfo)
{
Name = strName;
DescInfo = strDescInfo;
Thread.Sleep(1000);
}
/// <summary>
/// 系统名称
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// 系统描述
/// </summary>
public string DescInfo
{
get;
set;
}
public override string ToString()
{
return $"Name:{Name},DescInfo:{DescInfo}";
}
}
界面端多线程使用单例;
public int Num
{
get
{
return Int32.Parse(txtNum.Text);
}
set
{
txtNum.Text = value.ToString();
}
}
private void btnMulitThreadSingleton_Click(object sender, EventArgs e)
{
for (int i = 0; i < Num; i++)
{
Task pTask = new Task(() =>
{
ShowSystemConfig();
});
pTask.Start();
}
}
public void ShowSystemConfig()
{
Console.WriteLine($"对象实例ID:{SystemConfig.CurrentSystemConfig.GetHashCode()} 信息{SystemConfig.CurrentSystemConfig}");
}
出现问题如下:
可以看到在一开始的实例中出现的对象的HashCode值不是一个。若我们现在需要解决此问题便是使用线程锁解决此问题。
-
增加锁解决线程同步和线程安全。
/// <summary> /// 系统设置信息 /// </summary> public class SystemConfig { private static string SystemName = "单例实现"; private static string SystemDescInfo = "介绍单例模式使用过程中各种细节"; private static SystemConfig m_CurrentSystemConfig = null; private static Object MutilThreadLock = new Object(); public static SystemConfig CurrentSystemConfig { get { lock (MutilThreadLock) { if (m_CurrentSystemConfig == null) { m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo); } } return m_CurrentSystemConfig; } set { m_CurrentSystemConfig = value; } } }
但是使用该锁文件来解决问题,但是多线程会在大并发访问对象时会比较慢,因为线程的访问均会lock住,所以会造成锁定的问题。
-
使用双重严重锁解决该问题
/// <summary> /// 系统设置信息 /// </summary> public class SystemConfig { private static string SystemName = "单例实现"; private static string SystemDescInfo = "介绍单例模式使用过程中各种细节"; private static SystemConfig m_CurrentSystemConfig = null; private static Object MutilThreadLock = new Object(); public static SystemConfig CurrentSystemConfig { get { if (m_CurrentSystemConfig == null) { lock (MutilThreadLock) { if (m_CurrentSystemConfig == null) { m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo); } } } return m_CurrentSystemConfig; } set { m_CurrentSystemConfig = value; } } }
6.V5线程单例实现方式
线程单例和单例模式有一定的区别,当软件中全局只用一个实例信息时,单例模式完全可以使用6中的解决方案便可以应对。而当各个线程中只需要一个实例时,主线程与各个子线程使用相同的对象不同的实例,以解决多线程中的业务场景。
public class MutilSystemConfig
{
private static string SystemName = "单例实现";
private static string SystemDescInfo = "介绍单例模式使用过程中各种细节";
/// <summary>
/// 线程数据槽
/// </summary>
private static string key = "MutilSystemConfig-Single";
private static Object SingeletonLock = new Object();
public static MutilSystemConfig CurrentSystemConfig
{
get
{
MutilSystemConfig pMutilSystemConfig = CallContext.GetData(key) as MutilSystemConfig;
if (pMutilSystemConfig == null)
{
lock (SingeletonLock)
{
MutilSystemConfig pTempMutilSystemConfig = new MutilSystemConfig(SystemName, SystemDescInfo, Thread.CurrentThread.ManagedThreadId);
pMutilSystemConfig = CallContext.GetData(key) as MutilSystemConfig;
if (pMutilSystemConfig == null)
{
pMutilSystemConfig = pTempMutilSystemConfig;
CallContext.SetData(key, pMutilSystemConfig);
}
}
}
return pMutilSystemConfig;
}
}
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public MutilSystemConfig(string strName, string strDescInfo,int currentThreadID)
{
Name = strName;
DescInfo = strDescInfo;
CurrentThreadID = currentThreadID;
}
/// <summary>
/// 系统名称
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// 系统描述
/// </summary>
public string DescInfo
{
get;
set;
}
public int CurrentThreadID
{
get;
set;
}
public override string ToString()
{
return $"CurrentThreadID:{CurrentThreadID}";
}
}
界面调用:
private void btnMulitThreadSingleton_Click_1(object sender, EventArgs e)
{
for (int i = 0; i < Num; i++)
{
Task pTask = new Task(() =>
{
ShowMutilSystemConfig();
});
pTask.Start();
}
}
public void ShowMutilSystemConfig()
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId.ToString()} 对象实例ID:{MutilSystemConfig.CurrentSystemConfig.GetHashCode()} 信息{MutilSystemConfig.CurrentSystemConfig} ");
}
实现效果如下:
上述代码以及截图可以看到每一个线程都是一个线程实例信息。
设计思路:
- 通过线程ID形成字典信息,每一个线程判断一下线程字典中是否包含该对象,存在则返回一个对象。那么后台存储信息则保存为Dictionary<int,object>
- 当线程销毁时需要将Dictionary字典中的信息移除。
之所以需要移除字典项我们观察一下上图可以看到线程ID不是唯一的,当线程开启后,CPU会为线程随机分配一个不在当前线程池中的ID信息。也就是说线程ID会出现重复,在严谨意义上不同时间线上的线程ID相同不能判断时同一个线程。
基于此思路,但是在.Net框架中提供了一个数据结构可以完成该业务需求,为[数据槽]也就是上文CallContext.GetData(key)。
7.单例模式C#中的小技巧
-
静态构造函数Static Object() 天生的单例实现方式。
-
使用单例封装之后可以将对象闭包,将对象的实例化函数私有化private不允许其他人实例生成,避免其他调用人员再生成而导致代码上的不整洁或者为后续重构升级挖坑:
public class MutilSystemConfig
{
private static string SystemName = “单例实现”;
private static string SystemDescInfo = “介绍单例模式使用过程中各种细节”;/// <summary> /// 线程数据槽 /// </summary> private static string key = "MutilSystemConfig-Single"; private static Object SingeletonLock = new Object(); public static MutilSystemConfig CurrentSystemConfig { get { MutilSystemConfig pMutilSystemConfig = CallContext.GetData(key) as MutilSystemConfig; if (pMutilSystemConfig == null) { lock (SingeletonLock) { MutilSystemConfig pTempMutilSystemConfig = new MutilSystemConfig(SystemName, SystemDescInfo, Thread.CurrentThread.ManagedThreadId); pMutilSystemConfig = CallContext.GetData(key) as MutilSystemConfig; if (pMutilSystemConfig == null) { pMutilSystemConfig = pTempMutilSystemConfig; CallContext.SetData(key, pMutilSystemConfig); } } } return pMutilSystemConfig; } } /// <summary> /// 实例化系统设置信息 /// </summary> /// <param name="strName"></param> /// <param name="strDescInfo"></param> private MutilSystemConfig(string strName, string strDescInfo,int currentThreadID) { Name = strName; DescInfo = strDescInfo; CurrentThreadID = currentThreadID; }
}
JAVA
1.懒汉双重判断锁
单例代码实现
/// <summary>
/// 系统设置信息
/// </summary>
public class SystemConfig {
private static String SystemName = "单例实现";
private static String SystemDescInfo = "介绍单例模式使用过程中各种细节";
private static SystemConfig m_CurrentSystemConfig = null;
public static SystemConfig GetSystemConfig() {
if (m_CurrentSystemConfig == null) {
synchronized (SystemConfig.class) {
if (m_CurrentSystemConfig == null) {
m_CurrentSystemConfig = new SystemConfig(SystemName, SystemDescInfo);
}
}
}
return m_CurrentSystemConfig;
}
/// <summary>
/// 实例化系统设置信息
/// </summary>
/// <param name="strName"></param>
/// <param name="strDescInfo"></param>
public SystemConfig(String strName, String strDescInfo) {
Name = strName;
DescInfo = strDescInfo;
}
/// <summary>
/// 系统名称
/// </summary>
public String Name;
/// <summary>
/// 系统描述
/// </summary>
public String DescInfo;
@Override
public String toString(){
return "Name="+Name+" DescInfo="+DescInfo;
}
}
界面调用
package com.XCai;
public class Main {
public static void main(String[] args) {
for (int i=0;i<100;i++){
ThreadTest pThreadTest= new ThreadTest();
pThreadTest.start();
}
}
}
----线程
package com.XCai;
public class ThreadTest extends Thread {
public void run() {
System.out.println("对象HashCode"+SystemConfig.GetSystemConfig().hashCode()+"对象信息:"+SystemConfig.GetSystemConfig());
}
}