设计模式——单例模式
1. 概述
单例模式使用的要点就是保证对应类的实例全局唯一,这有利于协调系统整体的行为
2. 定义
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
3. 使用场景
避免产生多个对象消耗过多的资源或者只应该存在一个(类似于工具类),比如像处理IO或数据库资源的对象实例
4. UML类图

类图中体现出来的构成元素非常简单:
- 调用方
- 单例类(Singleton)
单例类实现的关键:
-
构造函数不对外开放,通常设置为
private,外部无法直接new -
通过一个静态方法或者枚举返回单例类的实例
-
确保实例有且仅有一个,注意考虑多线程的场景
-
确保单例类对象在反序列化时不会重新构建
5. 简单示例
5.1. 饿汉式
实践出真知,接下来通过简单的例子了解一下单例模式的核心思想
public class Enemy {
void attack() {
System.out.println(this + "攻击主角");
}
}
public class Boss extends Enemy { // 饿汉式
private static final Boss mInstance = new Boss(); // 唯一的实例,也有一种采用static{}的写法
public static Boss getInstance() {
return mInstance;
}
private Boss() { // 只能在类内部用new
}
}
public class Test {
public static void main(String[] args) {
Enemy enemy1 = new Enemy();
enemy1.attack();
Enemy enemy2 = new Enemy();
enemy2.attack();
Boss boss1 = Boss.getInstance();
boss1.attack();
Boss boss2 = Boss.getInstance();
boss2.attack();
}
}

以上写法属于饿汉式,饿汉式的特点便是甭管用不用,都会创建一个实例
饿汉式是利用ClassLoader机制确保线程安全的,只要类加载就会去实例化,但是这样的做法会造成一定程度的内存浪费,因为不一定会用到实例,没有按需加载
5.2. 懒汉式
懒汉式注重的就是”懒“,能躺着就不会坐着,只有需要才会做,这是最大程度地利用资源
如果体现到游戏中,饿汉式的思想就是游戏一加载,可能就给Boss创建出来了,明显有些浪费,因为就算创建出来玩家一开始也碰不到
等到加载到对应关卡,才去创建才更加合理些,这就是懒加载的思想
接下来改下Boss的代码
public class Boss extends Enemy {
private static Boss mInstance; // 声明但并未直接实例化
public static Boss getInstance() {
if (mInstance == null) { // 没有才创建
mInstance = new Boss();
}
return mInstance; // 有就直接给你
}
private Boss() {
}
}
主要的修改在于mInstance成员和getInstance()方法,现在是先声明,然后用的时候没有,才去new一个,后面的有了这个实例,就可以直接返回给你用了
似乎看上去挺合理的,但是别忘了多线程场景
因为一个线程去new对象的时候,很可能另一线程也判断mInstance == null,于是它不知道线程1去new了,于是线程2也去new,这样就有了2个实例,当然,也可能更多

Runnable r = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Boss boss = Boss.getInstance();
boss.attack();
}
};
for (int i = 0; i < 1000; i++) { // 1000个线程
Thread t = new Thread(r, "线程" + i);
t.start();
}

线程多的时候就增加了出现这种问题的可能性,并且本身在代码层面上也是有疏漏的,只能说在单线程上是可以保持单例的
既然出现了线程安全问题,那么就需要考虑去解决呀,于是乎,加上synchronized来解决
public static synchronized Boss getInstance() { // 只要调用该方法,线程就会强制排队
if (mInstance == null) {
mInstance = new Boss();
}
return mInstance;
}
这样的确是解决了线程安全问题,但是,这样就大大降低了多线程的效率,因为getInstance()可能会频繁使用到,其实只需要确保创建的那次不出问题就行了
5.3. 双重检查(DCL)
由之前懒汉式的问题出发,首先是线程安全问题,这里用到volatile关键字,这样该变量的修改多线程间就可见了
第一次是判断实例,防止非必要同步,第一次可能进来多个线程
然后进入同步块,这里一次只能进来一个,第一个进来的判断为空,创建,立即更新并对其他线程可见,已经进来的发现无事可做,但会走同步块,没进第一个的则直接返回实例
public class Boss extends Enemy {
private static volatile Boss mInstance; // volatile保证线程间可见性
public static synchronized Boss getInstance() {
if (mInstance == null) { // 把更改刷新,防止多同步
synchronized (Boss.class) { // 保证一次一个线程
if (mInstance == null) {
mInstance = new Boss();
}
}
}
return mInstance;
}
private Boss() {
}
}

5.4. 静态内部类
这是充分利用了类加载,在getInstance()调用时装载内部类完成初始化,以此保证线程安全和懒加载
public class Boss extends Enemy {
public static synchronized Boss getInstance() { // 调用时装载
return BossHolder.mInstance;
}
private Boss() {
}
private static class BossHolder { // 静态内部类
private static final Boss mInstance = new Boss();
}
}
5.5. 枚举
终极而简便的方式,利用枚举的特性,能够在创建实例时保证线程安全,即使是在反序列化过程也能保证单例,而不需要像之前几种一样,需要额外进行处理
enum Boss {
INSTANCE;
public void attack() {
System.out.println(this.hashCode() + "攻击主角");
}
}
6. Android场景
单例模式应用的场景很广泛,这里给出一个WindowManager相关的例子,对应的类WindowManagerGlobal,完成WindowManagerImpl各个方法的具体实现,与WMS的窗口处理绘制流程相关
可以发现,这里使用的正好是线程安全的懒汉式,使用同步块包裹住了判空和实例的创建,保证多个线程的环境下只返回同一实例