单例模式:
作用:确保一个类只有一个实例,并提供一个全局访问的接口
实现:采用静态变量保存实例,并私有化构造函数以确保其唯一性,同时提供一个静态方法用于访问实例。
特性:
- 如果我们不使用它,就不会创建实例。节省内存和CPU周期始终是好的。既然单例只在第一次被访问的时候初始化,那么如果我们的游戏始终不使用它,它就不会吃时候。
- 它在运行时初始化。静态类是单例常见的替代品,但是静态类只能自动初始化。编译器早在main函数调用之前就初始化静态数据了。这意味着他不能利用那些只有游戏运行起来才能知道的信息(从文件中载入配置)。这意味着静态类之间不能相互依赖,因为编译器不能保证初始化顺序。而单例可以延迟初始化,只要不是循环依赖,单例可以在初始化时引用另一个单例。
- 可以继承单例。将文件系统跨平台,在不同平台派生类。
后悔使用单例的原因:
1它是一个全局变量
- 它依然保持着全局状态,是一个全局变量。全局变量的害处:
(1)令代码晦涩难懂。假如我们正在跟踪其他人写的函数中的bug,没有全局变量,只需要理解函数体和它传递的参数。如果涉及到全局变量的访问,我们需要在所有代码中查找该全局变量。 - 促进耦合。如果程序中存在全局实例,开发人员可能会为了方便直接访问它,导致架构被打乱,可维护性受到影响。
- 对并发不友好。当设置全局变量时,我们创建了一段内存,每个线程都能够访问和修改它,这会导致条件竞争、死锁和其他的线程同步bug。
它同样存在全局变量存在的问题。
2 它是一个画蛇添足的解决方案
- 单例的两个功能是绑定在一起的,但是我们使用单例的主要原因是,便利访问。
- 比如,要实现Log类,这会限制我们不能够创建多个日志器。我们会将应用于不同模块的日志编写在一起,降低了代码的可读性。
3 延迟初始化剥离了你的控制
- 实例化需要花费一定的时间(分配内存,加载资源),如,第一次加载播放声音的时候延迟初始化,可能带来卡顿。
- 游戏通常需要仔细地控制内存在堆中的布局来防止碎片化。我们需要知道初始化发生的时间,以便控制它在堆中的内存布局。
4 该怎么做
- 看你究竟是否需要类。遵循单一职责原则,如,子弹类的属性设置和移动等功能交由自身完成,不需要管理类。
- 将类限制为单一实例。如果只是想要限制为单一实例,只需要加一个标识,每次访问时先判断标识是否为true,不足之处是运行时检测来防止多个实例,单例是在编译期确定。
- 为实例提供便携的访问方式。
(1)传递进去。将对象作为参数传递
(2)在基类中获取它。私有化实例,提供一个protected权限的访问方法
(3)通过其他全局对象访问。将全局对象类包装到现有类里面来减少它们的数量。
(4)通过服务器定位来访问。
懒汉和饿汉
区别在于懒汉模式在第一次调用GetInstance时,进行内存的分配,饿汉则在定义时就进行了内存分配。
线程安全
如何分辨懒汉模式和饿汉模式是否线程安全,首先确认什么情况会导致线程不安全。
- 线程的调度是抢占式执行。
- 修改操作不是原子的
- 多个线程同时修改同一个变量
- 内存可见性
- 指令重排序(java)
由于懒汉模式造成了多个线程同时修改同一个变量这一情况,所以说懒汉模式是线程不安全的。
public class Single2 {
//懒汉模式
static class Singleton {
private Singleton() {}
privatestatic Singleton instance = null;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
然而在上面的代码中,即使instance已经实例化了,但是每次调用getInstance()还是会涉及加锁解锁,实际上此时已经不需要了,所以要实现在instance实例化之前调用的时候加锁,之后不加锁,就引出了双重检验锁版本。
public class Single2 {
//懒汉模式
static class Singleton {
private Singleton() {}
private static Singleton instance = null;
public static Singleton getInstance() {
//双重检验锁
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
}
编译器优化:
只有第一次读操作从内存中读取数据同时存放在CPU的寄存器中,因为从寄存器中读取数据速度远大于从内存中读取,所以后续的读操作就直接从寄存器中读取数据。
此时我们已经基本完成懒汉模式的设计,但是懒汉模式在多线程情况下由于编译器优化还会出现一种特殊情况,某个线程可能会进行多次读操作:
线程1第一次读取instance为空,而线程2未释放锁时,加锁失败,与此同时其他线程也读取到instance为空,于是实例化instance后线程2释放锁,此时线程1加锁成功,但读取instance仍然为null。此时的线程2进行了多次读操作,然而由于编译器优化,导致线程2没有读到最新的数据,即实例化的instance。
此时需要使用volatile来解决这种特殊情况带来的问题。
volatile的作用:保持内存可见性,禁止编译器进行某种场景的优化(一个线程在读,一个线程在写,修改对于读线程来说可能没有生效)
懒汉模式最终版本:
public class Single2 {
//懒汉模式
static class Singleton {
private Singleton() {}
//volatile 避免内存可见性引出的问题
private volatile static Singleton instance = null;
public static Singleton getInstance() {
//双重检验锁
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
}
总结:
为了保证懒汉模式线程安全,涉及到三个要点:
-
加锁保证线程安全
-
双重if保证效率
-
volatile避免内存可见性引来的问题.