深入理解单例模式的几种实现方式
前言
单例模式是一种很常用的设计模式,其定义是单例对象的类只允许有一个实例存在。在使用spring自动创建对象时默认就是单例的。
使用场景
需要频繁的对对象进行创建与销毁,如果工具类对象
一、饿汉式(静态变量)
public class Singleton1 { private static final Singleton1 INSTANCE = new Singleton1(); //静态工厂 public static Singleton1 getInstance(){ return INSTANCE; } }
饿汉式就是在类加载初始化的时候就创建了对象,尽管你还不需要使用他。相对来说我是比较喜欢这种方式的,既没有线程安全,速度也快,当然这是以牺牲空间换取速度,大家可以根据实际情况,根据该对象被创建的概率酌情使用该方法。
二、饿汉式(静态代码块)
public class Singleton2 { private static Singleton2 instance = null; //静态代码块 static { instance = new Singleton2(); } public Singleton2 getInstance(){ return instance; } }
静态代码块实现的饿汉式与上述第一种方式类似,都是在类初始化的时候就创建对象,同样是线程安全的,其实就是JVM帮我们避免了线程安全,因为类只要加载一次,所以只会创建一个对象。
三、懒汉式(多线程下不可用)
public class Singleton3 { private static Singleton3 instance = null; public static Singleton3 getInstance(){ if (null == instance){ //(1) instance = new Singleton3(); //(2) } return instance; } }
这个懒汉式写法是线程不安全的,只能在单线程情况下使用。假设有两个线程A和B,线程A运行到(1)行代码时时间片已经完了,此时CPU切换执行线程B,线程B完成整个流程,也就是说这个时候instance已经不为空了,然后再执行线程A,此时线程A就会执行(2)行语句重新创建新的实例。
四、懒汉式(多线程下可用,但效率低)
public class Singleton4 { private static Singleton4 instance = null; public static synchronized Singleton4 getInstance() { if (null == instance){ instance = new Singleton4(); } return instance; } }
第四个方法与第三个方法的区别在于在getInstance方法中加了synchronized同步锁,这样会导致只有等一个线程运行完后另一个线程才能进入该方法,当然不会有线程安全的问题,但我们只是在一开始实例化对象时需要,之后只需要直接返回对象即可,这样相当于每次都要去查询对象是否实例化,且还要排队查询,这样显然很影响性能,相当于每个线程都要排队执行该方法。所以不推荐使用这种写法。
五、静态内部类
public class Singleton5 { private static class insideClass{ private static final Singleton5 instance = new Singleton5(); } public static Singleton5 getInstance(){ return insideClass.instance; } }
静态内部类与懒汉式类似,只不过是JVM帮我们解决了多线程的问题。在调用getInstance()方法是会加载内部类insideClass,也就会实例化instance,ClassLoader类加载机制会帮我们保证只有一个线程去初始化该内部类,也就只会生成一个实例。看起来这个方法又是懒加载,又是线程安全好像很完美的样子,但设计是要落地与业务的,没有最好的写法,只有最合适的写法。假设创建这个对象花费的时间很长,这无疑会给初次调用getInstance()方法的用户带来不好的体验,那么这种写法也是不可取的。
六、双重检查
public class Singleton6 { private static volatile Singleton6 instance = null; //(1) private Singleton6(){} public static Singleton6 getInstance(){ if (null == instance){ synchronized (Singleton6.class){ if (null == instance){ instance = new Singleton6(); } } } return instance; } }
Double-Check在多线程编程中是很常见的。通过两次检查和静态代码块来限制同一时间只有个线程在实例化对象,进而控制只有一个实例产生。看起来貌似很完美,但大家有没有看到(1)行中的volatile关键字呢?他又有什么作用呢?
双重检查的写法在JDK1.5是有问题的,因为JVM优化会对指令进行重排序。我们理想中创建一个对象的过程是:a->b->c
a.JVM分配内存空间
b.初始化对象
c.将instance指向分配的内存空间
但实际上执行的顺序可能是 a->c->b
假设有线程A和线程B,线程A执行new Singleton6(),指令执行的顺序是a->c->b,此时只是将instance执行分配的内存空间,也就是说instance此时不等于null,这个时候线程B获取instance实例去执行操作,但实际上对应的内存空间还没有初始化,那么将会出现错误。