Java 高效编程·单例模式的几种实现
单例模式的几种实现
单例模式是一种对象创建模式,它用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。Java 里面实现的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的 ClassLoad 装载实现单例类的时候就会创建一个类的实例。
单例模式的优点:
- 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
单例模式的核心在于通过一个接口返回唯一的对象实例。首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类的实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法。
饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInsatnce() {
return instance;
}
}
饿汉式提前实例化,没有懒汉式中多线程问题,但无法对 instance 实例做延时加载,单例会在加载类后一开始就被初始化,保证该实例只有一个,不管是否调用 getInstance()
都会存在一个实例在内存中。
此外饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance()
之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
懒汉式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式实现了实例延时加载,但是需要添加 synchronized
来保证线程安全。但这样做并不高效,因为在任何时候只能有一个线程调用 getInstance()
方法。
双重检验锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法,可以看作是上面懒汉式的改进版。双重检查锁会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。第一个 if 可以提升效率,为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) { // Single Checked
synchronized (Singleton.class) {
if (instance == null) { // Double Checked
instance = new Singleton();
}
}
}
return instance;
}
}
双重检验锁看起来很完美,但可惜它有问题。主要在于 instance = new Singleton()
这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序可能是 1-2-3
也可能是 1-3-2
。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
这时需要将 instance 变量声明成 volatile
就可以了:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getSingleton() {
if (instance == null) { // Single Checked
synchronized (Singleton.class) {
if (instance == null) { // Double Checked
instance = new Singleton();
}
}
}
return instance ;
}
}
使用 volatile
的主要原因是其一个特性:禁止指令重排序优化。也就是说,在 volatile
变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3
之后或者 1-3-2
之后,不存在执行到 1-3
然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile
变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
静态内部类
推荐使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种写法使用 JVM 本身机制保证了线程安全问题;由于 SingletonHolder
是私有的,除了 getInstance()
之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举
同样也是《Effective Java》推荐的写法,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,但使用较少。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
调用:
Singleton.INSTANCE.whateverMethod();
参考文章:
Java 编程设计模式-单例模式
如何正确地写出单例模式
金点网络-全网资源,一网打尽 » Java 高效编程·单例模式的几种实现
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。
- 是否提供免费更新服务?
- 持续更新,永久免费
- 是否经过安全检测?
- 安全无毒,放心食用