小编将会列举出五种单例模式的设计方案,以及如何通过反射机制打破单例模式。
单例模式
1. 单例模式的概念:
为了保证类中的对象在内存中是全局唯一性给定的一种设计规则
2.单例模式的应用场景?
- 有频繁创建对象然后销毁的情况
- 创建对象耗时过多或者说消耗资源过多,但又经常使用该对象
- 频繁访问IO资源,例如:连接池对象的应用
3.单例模式应用的目的?
减少对象在内存中资源的占用
4.单例模式的具体实现思路?
规则:
- 类的外部不允许直接构建此类对象
- 类的外部只能通过静态方法访问此对象
实现:
- 将构造方法私有化
- 在类的内部构建对象
- 定义一个静态方法,通过这个方法直接返回此对象
5.设计方案
5.1 设计方案一:
public class Singleton{
//单例对象
private static Singleton instance = null;
//私有构造函数
private Singleton(){
}
//静态工厂方法
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
【缺点】:存在线程安全问题,只适用于单线程。
假设线程A访问getInstance()方法,Singleton类刚刚被初始化,线程B进来,由于instance是为空,所以通过了天剑判断,开始执行new操作。
5.2 设计方案二:
public class Singleton{
//单例对象
private static Singleton instance;
//私有构造函数
private Singleton(){
}
//静态工厂方法被加锁,会造成堵塞
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
【缺点】:在多线程情况下会出现性能问题。
5.3 设计方案三:
public class Singleton{
//单例对象,类加载时创建
private static Singleton instance = new Singleton();
//私有构造函数
private Singleton(){
}
//此方法没有堵塞问题适合对象占用内存比较小,适合频繁访问
public static Singleton getInstance(){
return instance;
}
}
【缺点】:假如对象比较大,类加载时就创建此对象,而不去使用,则长时间占用内存资源。
5.4 设计方案四:
public class Singleton{
//私有构造函数
private Singleton(){
}
static class Lazy{
public static final Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return Lazy.singleton;
}
}
【注意】:
- 从外部无法访问静态内部类Lazy,只有当调用Singleton.getInstance方法的时候,才能得到单例对象singleton。
- singleton对象初始化的时候并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类Lazy被加载的时候。因此这种实现方式是利用 classloader的加载机制 来实现懒加载,并保证构建单例的线程安全。
5.5 设计方案五:
public class Singleton{
//单例对象
private static Singleton instance = null;
//私有构造函数
private Singleton(){
}
//静态工厂方法
public static Singleton getInstance(){
//双重检测机制
if(instance == null){
//对Singleton对象加锁实现同步
synchronized(Singleton.class){
//双重检测机制
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
【缺点】:可能会出现指令重排的问题
这种情况看起来貌似没有问题:当instance还没被线程A创建时,线程B执行第一个双重检测if(instance == null),得到true,而当instance已经被线程A构建完成了,线程B执行第二个双重检测if(instance == null)的时候会得到false吗?其实并不会,这里涉及到JVM的 指令重排 问题,可能得到true。
什么叫指令重排?
简单说就是:在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列。
【例如】:instance = new Singleton(); 会被编译器编译成如下JVM指令.
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
创建一个对象的顺序是这样子的,而这些指令如果经过JVM和CPU的优化,指令重排顺序可能会变成如下顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象
【问题解决】:
针对以上指令重排造成的安全性问题,我们可以通过volatile修饰符阻止变量访问前后出现的指令重排的问题,也就保证了执行顺序。
5.6 设计方案六:
public class Singleton {
//单例对象
private volatile static Singleton instance = null;
//私有构造函数
private Singleton() {}
//静态工厂方法
public static Singleton getInstance() {
//双重检测机制
if (instance == null) {
//同步锁
synchronized (Singleton.class){
//双重检测机制
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这样子在B线程看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
6. 如何通过反射打破单例模式?
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//将私有构造设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
简单归纳为三个步骤:
-
获得单例类的构造器。
-
把构造器设置为可访问。
-
使用newInstance方法构造对象。
最后为了确认这两个对象是否真的是不同的对象,我们使用equals方法进行比较。毫无疑问,比较结果是false。