单例模式属于创建型模式,它提供了一种创建对象的最佳方式;
1.介绍
单例模式涉及到单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。 这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象;
一个全局使用的类频繁地创建与销毁
判断系统是否已经有这个实例化对象,如果有直接返回,如果没有则创建;
构造函数要求是私有的 private,这样该类就不会被实例化;
[1]在内存中只有一个实例,减少了内存开销,尤其是频繁地创建和销毁实例 [2]避免对资源的多重占用[TODO 解释];
2.实现方式
2.1 懒汉式
2.1.1 线程不安全的懒汉式
public class Singleton { private static Singleton instance = null; /** * 私有化构造器,这样该类就不会被实例化; */ private Singleton() { } public static Singleton getInstance() { //判断是否为空,如果是创建实例,如果不是直接返回; if (instance == null) { // JVM 重排序导致的空指针问题; // 1.在堆中开辟内存空间 // 2.在堆内存中实例化Singleton(); // 3.把对象指向堆空间; // 由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,// 会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。 instance = new Singleton(); } return instance; }}
多线程访问getInstance()方法都会得到实例,从严格意义上来讲,它不算单例模式;
2.1.2 线程安全的懒汉式
public class Singleton { private static Singleton instance = null; /** * 私有化构造器,这样该类就不会被实例化; * ; */ private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}
- 特性:
- 延迟加载
- 线程安全
- 效率低下 : 必须加锁 synchronized 才能保证单例,但加重量级锁比较影响效率
- 避免内存浪费 :第一次调用才初始化,避免了内存浪费;
2.2 饿汉式
public class Singleton { private static Singleton instance = new Singleton(); /** * 私有化构造器,这样该类就不会被实例化; * ; */ private Singleton() { } public static Singleton getInstance() { return instance; }}
采用饿汉模式,单实例对象便会在类加载完成之时,常驻堆中,后续访问时本质上是通过该类的Class对象嵌入的intance指针寻址,找到单实例对象的所在。 这一模式的好处在于: 1、通过空间换时间,避免了后续访问时由于对象的构造带来的时间上的开销; 2、(WHY) 无需考虑多线程的并发问题,JVM在类加载过程中,会通过内部加锁机制保证加载类的全局唯一性。 不好的地方,就是不管你用还是不用,只要完成了类加载,Heap中单实例对象所占的内存空间就被占据了, 某种程度上,也是内存泄漏的体现。这也是采用『饿汉模式』的由来。
2.3 静态内部类
public class Singleton { private Singleton() { } private static class SingleTonHolder { private static Singleton singleton = new Singleton(); } private static Singleton getInstance() { return SingleTonHolder.singleton; }}
2.4 双重校验锁(DCL)
public class Singleton { // volatile 禁止指令重排序 private static volatile Singleton instance; /** * 私有化构造器,这样该类就不会被实例化; */ private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); return instance; } } } return instance; }}
2.4.1 双重锁机制分析
我们假设一种情况,就是当两个线程同时到达,即同时调用getInstance()方法. 此时由于singleTon == null,所以很明显,两个线程都可以通过第一重的检查(instance==null); 之后,由于锁机制的存在,所以只会有一个线程进入到临界区中,另一个线程只能在外面等待; 而当第一个线程执行完new Singleton()语句之后,第二个线程便可以进去到临界区中; 此时,如果没有第二道instance==null的判断, 那么第二个线程还是可以调用new Singleton()语句去生成新的实例化对象 ,这就违背了单例模式的初衷。
当我们去掉第一个非空判断后,程序在多线程情况下还是可以完好的运行的; 在不考虑第一个判断的情况下: 当两个线程同时到达,由于锁机制的存在,第一个线程进入临界区之后去初始化new SingeTon并给instance赋值,第二个线程则等待,当第一个线程退出 lock 语句块时, singleTon 这个静态变量已不为 null 了,所以当第二个线程进入 lock 时, 还是会被第二重 singleton == null 挡在外面,而无法执行 new Singleton(),
- 既然在没有第一个判断的情况下,单例也可以实施,那为什么需要第一道判断呢?
这里就涉及一个性能问题了,因为对于单例模式的话,new SingleTon()只需要执行一次就 OK 了,而如果没有第一重 singleTon == null 的话,每一次有线程进入 getInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重 singleTon == null 的话,那么就只有在第一次,也就是 singleTton ==null成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回 Singleton 实例就 OK 了而根本无需再进入 lock 语句块了,这样就可以解决由线程同步带来的性能问题了。
2.4.2 双重锁机制失效分析
[1]我们先引入一个概念:指令重排序(具体会在多线程的锁机制中详解); 所谓指令重排序是指在不改变 原语义的情况下,通过调整指令的执行顺序让程序执行地更快; [2]双重锁的问题在于 由于指令重排序的存在,导致初始化Singleton()的过程和将对象地址赋给instance字段的顺序是不确定的。 在某个线程创建单例对象的过程中,在构造器被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值.此时,将分配的内存地址复制给instance字段[栈指针指向堆内存的过程],然而该对象还没有初始化。若与此同时,另外一个线程来调用getInstance()方法 ,那么导致的结果就是取到的不是正确的对象。 [3] 在JDK 1.5版本之后引入了 volatile关键字. 它对于我们来讲 ,实现了内存可见性,确保实例每次都会从主存中读取,以及禁止指令重排序 也就是保证了instance变量被赋值的时候是确保已经被初始化过的;