单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。当系统需要某个类只能有一个实例时,就可以采用单例模式。
保证单例模式仅有一个实例的核心思想是构造方法私有化,即不允许外部调用该类的构造方法。基于此思想,主要有以下两种实现方式:
直接实例化
直接实例化这种方式也称作“饿汉式”,它直接定义了静态成员变量 s,并通过 new Singleton()
完成了初始化,之后不再变化,是线程安全的。
这种方式也存在一定的资源浪费,当没有使用 Singleton 对象时,程序依然会创建 Singleton 对象。
public class Singleton {
private Singleton() {}
private static final Singleton s = new Singleton();
public static Singleton getInstance() {
return s;
}
}
延迟实例化
既然直接实例化浪费资源,那么我们是否可以考虑,在程序需要该对象的时候才创建它呢?当然可以!
与直接实例化稍不同,单例成员变量 s 初始为 null,它在方法 getInstance()
内部完成延迟实例化,并返回单例对象。
public class Singleton {
private Singleton() {}
private static Singleton s = null;
public static Singleton getInstance() {
if (s == null) {
s = new Singleton();
}
return s;
}
}
这种方式存在线程安全问题。例如,假设两个线程调用 getInstance()
方法,线程 1 执行完 if(s == null)
,条件成立,在执行实例化语句 s = new Singleton()
之前,线程 2 来了,此时线程 2 执行 if(s == null)
,依然成立,进入 if 语句体。这种情况带来的后果是:程序两次创建了对象,这并不符合我们对单例模式的定义。
针对这种情况,可以有以下四种解决方法:
完全同步
完全同步方法,是在方法上加上 synchronized
同步。当多线程同时访问 getInstance()
方法时,多线程是“串行”的。
public class Singleton {
private Singleton() {}
private static Singleton s = null;
public static synchronized Singleton getInstance() {
if (s == null) {
s = new Singleton();
}
return s;
}
}
这种方法,多线程每次访问 getInstance()
都必须“串行”运行,效率比较低。
部分同步
部分同步方法通过双重锁部分同步机制获得单例对象。因为代码中有两行相同的语句 if(s == null)
,故而叫做双重锁。第一个 if 语句可并行,当多线程均满足该条件, synchronized
修饰的代码必须串行运行。这样的话,其实只需要在第一次创建对象(通过了第一个 if 判断)的时候进行同步,效率较高。
public class Singleton {
private Singleton() {}
private volatile static Singleton s = null;
public static Singleton getInstance() {
if (s == null) {
synchronized(Singleton.class) {
if (s == null) {
s = new Singleton();
}
}
}
return s;
}
}
注意,volatile
关键字是确保当 s 被初始化成 Singleton 实例时,多个线程可以正确处理 s,即内存可见性。
静态内部类
通过静态内部类 Inner
来实现单例对象。虚拟机加载应用程序字节码时,单例对象并不会立即创建,当第一次运行 Inner.s
时,单例对象才动态生成。这种实现方式无 synchronized
关键字,提高了效率。
public class Singleton {
private Singleton() {}
private static class Inner {
private static final Singleton s = new Singleton();
}
public static Singleton getInstance() {
return Inner.s;
}
}
枚举
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。调用的时候只需要 Singleton.INSTANCE
即可。
public enum Singleton {
INSTANCE;
// var here
public int var;
// methods here
public void otherMethods() {
System.out.println("write other methods here...");
}
}
enum 实现 Singleton 的三个特性:自由序列化、线程安全、保证单例。
首先, enum 是由 class 实现的,它可以有 member 和 member function。另外,由于 enum 是通过继承 Enum 类实现的,enum 结构不能作为子类继承其他类,但可以用来实现接口。此外 enum 类不能被继承,在反编译中,可以发现该类由 final 修饰。
其次,enum 有且仅有 private 的构造器,防止外部的额外构造,这恰好与单例模式吻合。
而对于序列化和反序列化,因为每一个枚举类型和枚举变量在 JVM 中都是唯一的,即 Java在序列化和反序列化枚举时做了特殊的规定,枚举的 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject 会破坏单例的问题。
(完)
参考资料
👉 《Head First 设计模式》
👉 《Java 设计模式及应用案例分析》
👉 《Java 枚举 enum 以及应用:枚举实现单例模式》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。