Java 单例模式
单例模式(Singleton)是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。提供一个访问该实例的全局方法(通常名为 getInstance()
)。
Java 里面实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的 ClassLoad 装载实现单例类的时候就会创建一个类的实例。
在 Java 语言中,这样的行为能带来两大好处:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
因此对于系统的关键组件和被频繁使用的对象,使用单例模式可以有效地改善系统的性能。
单例模式的核心在于通过一个接口返回唯一的对象实例。
首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类的实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法。
单例模式基本实现
public class Singleton {
private Singleton() {}
private static Singleton instance = new Singleton();
public static Singleton getInsatnce() {
return instance;
}
}
几个关键点:
- private 的构造函数是防止单例被其它代码实例化(实例化动作由类内部自行控制)。
- instance 成员必须是 static 定义的,只允许类拥有一个实例化对象(单例对象)。
几种单例模式对比
线程安全 | 并发性能好 | 支持延迟加载 | 序列化/反序列化安全 | 备注 | |
---|---|---|---|---|---|
饿汉式 | Y | Y | |||
懒汉式(不加锁) | Y | Y | |||
懒汉式(加锁) | Y | ||||
懒汉式(双重校验锁) | Y | Y | |||
懒汉式(双重校验锁 + volatile) | ( Y ) | Y | Y | Java 5 以上安全 | |
静态内部类 | Y | Y | Y | ||
枚举 | Y | Y | Y |
主要说明一下饿汉式和懒汉式:
饿汉式:指全局的单例实例(instance 的初始化)在类装载时构建。
懒汉式:指全局的单例实例(instance 的初始化)在第一次被使用时构建。
下面来详细讲讲每种单例模式。
I. 饿汉式
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance(){
return instance;
}
}
缺点: 提前装载会导致资源的不必要浪费。
II. 懒汉式(不加锁)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点: 多线程调用 getInstance
容易同时创建多个实例,不能正常工作。
II. 饿汉式(加锁)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
缺点: 多线程下调用耗时增加。
其实实例只会创建一次,却因为对整个 getInstance
接口加锁,明显浪费。
所以有 饿汉式(加锁) 的变种:
III 饿汉式(双重检验锁)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
缺点:由于 instance = new Singleton()
这一步并非原子操作,存在 instance
非 null,但 instance
还没初始化的情况。此时调用会引起出错。
instance = new Singleton()
主要步骤:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间(执行完 instance 就非 null 了)
但是 JVM 的即时编译器中存在指令重排序的优化。即上述第 2 和第 3 步的顺序并不能保证。
如果执行顺序为: 1 -> 3 -> 2。那么执行完第 3 步后, instance
非 null,此时其它线程访问时会在第一个 null 判断处返回 instance
。
所以还有 饿汉式(双重校验锁) 的变种:
IV 饿汉式(双重检验锁 + volatile)
public class Singleton {
private static volatile Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
volatile
并不是要使用其 “每次从主内存中读取信息,本地线程不保留 instance 副本” 的特性。
而是因为 volatile
的另一个特性:禁止指令重排序优化。
在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
这也就避免出现 1 -> 3 -> 2 因此 instance 没初始化的访问,导致出错的问题。
缺点:Java 5 以前的 JMM (Java 内存模型)存在缺陷,使用 volatile
也不能完全避免重排序。所以这种方式并不 “通用” 。
V. 静态内部类
public class Singleton {
private Singleton (){}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法使用 JVM 本身的机制保证线程安全问题;
由于 SingletonHolder 是私有的,除了 getInstance()
之外没有办法访问它;
且只有 getInstance()
访问时,第一次触发 SingletonHolder 的 class 加载才会生成 INSTANCE 实例,因此它也是懒汉式的;
同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
VI. 枚举
public enum Singleton {
INSTANCE;
}
通过 Singleton.INSTANCE
的访问形式访问(前面的单例访问 Singleton.getInstance()
)。
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
就是习惯性问题,对大多数人来说用 enum class 当普通 class 使用,会有不习惯。