单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
单例模式主要分两种
在类加载时已经创建好该单例对象。
public class Test_Instan
{
private static Test_Instan instant=new Test_Instan();
public static Test_Instan Instant
{
get
{
return instant;
}
}
private Test_Instan()
{
}
}
在真正需要使用对象时才去创建该单例类对象
public class Test_Instan
{
private static Test_Instan instant;
public static Test_Instan Instant
{
get
{
if (instant==null)
{
instant = new Test_Instan();
}
return instant;
}
}
private Test_Instan()
{
}
}
不足之处:如果两个线程同时判断instant为空那么它们都会去实例化一个Test_Instan对象,这就变成双例了。
改进:利用锁
public class Test_Instan
{ private Test_Instan()
{
}
private static Test_Instan instant;
private static object syncRoot = new object();
public static Test_Instan Instant
{
get
{
if (instant == null) // 假如线程A和线程B同时看到instant为null
{
lock (syncRoot)//线程A或者线程B获得该锁进行初始化,另一线程阻塞
{
if (instant == null)//其中一个线程进入该分支创建单例对象,另外一个阻塞线程,阻塞结束后进入该分支则会发现instant已被创建
{
instant = new Test_Instan();
}
}
}
return instant;
}
}
}
加双锁
public class Test_Instan
{ private Test_Instan()
{
}
private volatile static Test_Instan instant;
private static object syncRoot = new object();
public static Test_Instan Instant
{
get
{
if (instant == null) // 假如线程A和线程B同时看到instant为null
{
lock (syncRoot)//线程A或者线程B获得该锁进行初始化,另一线程阻塞
{
if (instant == null)//其中一个线程进入该分支创建单例对象,另外一个阻塞线程,阻塞结束后进入该分支则会发现instant已被创建
{
instant = new Test_Instan();
}
}
}
return instant;
}
}
}
为什么加双锁
if (instance == null) {
instance = new Singleton();//erro
}
如果不使用volatile关键字,隐患来自于上述代码中注释了 erro 的一行,这行代码大致有以下三个步骤:
在堆中开辟对象所需空间,分配地址
根据类加载的初始化顺序进行初始化
将内存地址返回给栈中的引用变量
由于编译器允许处理器乱序执行,所以第二步和第三步的顺序无法保证。如果第三步先执行完毕、第二步未执行时,有另外的线程调用了instance,由于已经赋值,将判断不为null,拿去直接使用,但其实构造函数还未执行,成员变量等字段都未初始化,直接使用,就会报错。
而对volatile变量的写操作,不允许和它之前的读写操作打乱顺序;对volatile变量的读操作,不允许和它之后的读写乱序。
当一个线程要使用共享内存中的volatile变量时,它会直接从主内存中读取,而不是使用自己本地内存中的副本。当一个线程对一个volatile变量进行写时,它会将这个共享变量值刷新到共享内存中。
类似与我们上面的饿汉式,不过这里要注意的是Awake函数的执行顺序是不可控的,通俗来说每次运行程序的时候每个脚本的执行顺序都不一样,通常我们会自己写一个初始函数函数,来控制我们的执行循序,确保单例的初始化在所有的逻辑之上。
public class SingletonUnity : MonoBehaviour
{
private static SingletonUnity instant;
public static SingletonUnity Instant
{
get
{
return instant;
}
}
private void Awake()
{
instant = this;
}
}
由于unity的所有继承自MonoBehaviour的脚本都必须挂在一个游戏对象上,否则无法执行,也谈不上用new来实例化,这点我们要尤为注意。
public class SingletonUnity : MonoBehaviour
{
private static SingletonUnity instant;
public static SingletonUnity Instant
{
get
{
if (instant==null)
{
instant= FindObjectOfType<SingletonUnity>();
if (instant==null)
{
GameObject instan_ = new GameObject();
instant = instan_.AddComponent<SingletonUnity>();
}
}
return instant;
}
}
}
对比我们上面纯C#的写法,在Unity创建的MonoBehaviour类的单例并没有对MonoBehaviour类的实例化进行非公有化,因为,在Unity中,MonoBehaviour类有可视化操作的特点,当我们手动拖拽多个单例脚本到游戏中,这时我们运行程序很可能会出现逻辑错误,我们单例模式的原则或者说目的是保证一个类只有一个实例,所以我们需要改进一下。
public class SingletonUnity : MonoBehaviour
{
private static SingletonUnity instant;
public static SingletonUnity Instant
{
get
{
if (instant == null)
{
SingletonUnity[] instants = FindObjectsOfType<SingletonUnity>();
for (int i = 0; i < instants.Length; i++)
{
Destroy(instants[i].gameObject);
}
GameObject instan_ = new GameObject();
instant = instan_.AddComponent<SingletonUnity>();
}
return instant;
}
}
}
最后写个测试脚本测试下
public class Client_sigle : MonoBehaviour {
void Start()
{
Debug.Log(SingletonUnity.Instant.name);
}
}
运行前
当然,你也可以按自己的需求,更改上面写法。
改进:上面单例的写法代码量比较多,当游戏中有多个单例,显然不可能用我们的复制粘贴,这时我们可以用泛型来增强我们的复用性。
public class SingletonUnity<T> : MonoBehaviour where T: MonoBehaviour
{
private static T instant;
public static T Instant
{
get
{
if (instant == null)
{
T[] instants = FindObjectsOfType<T>();
for (int i = 0; i < instants.Length; i++)
{
Destroy(instants[i].gameObject);
}
GameObject instan_ = new GameObject();
instant = instan_.AddComponent<T>();
}
return instant;
}
}
}
只需要让单例继承该类即可。