一.什么是单例
单例对象的类必须保证只有一个实例存在
对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:
懒汉式:指全局的单例实例在第一次被使用时构建。
饿汉式:指全局的单例实例在类装载时构建。
懒汉式单例
1.简单版本
缺点:当多线程工作的时候,如果有多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。
改进 ...
加上synchronized关键字
缺点:虽然解决了多个线程多个实例的问题 但是当一个线程执行到getInstance()时其他线程就要进入等待状态 实际上会对程序的执行效率造成负面影响
继续改进 ..
双重检查
实际上就是合并了上面两个解决办法
第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。
第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况
其实 还是有小概率会出现问题 主要是设计 原子操作 和 指令重排的概念
主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance == null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。
这里的关键在于——线程T1对instance的写操作没有完成,线程T2就执行了读操作。
继续改进....
终极版本 volatile关键字
对于上面的版本虽然已经很大程度上改善了 但是毕竟还是有几率会出现问题的
解决方案就是给instance生明加上volatile关键字
volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。
注意:volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。
到了这个版本 确实没什么问题了 但是就是太复杂了。。。。
上面介绍了 懒汉式的一些基本写法 和改进后的写法 下面介绍一些饿汉式 相比懒汉式就简单多了
饿汉式单例实现
注:开头说过 饿汉式单例是指全局的单例实例在类装载时构建的实现方式
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势能够免疫许多由多线程引起的问题
当然万事万物没有完美的 如果非要对上面挑点毛病出来就是 由于INSTANCE的初始化是在类加载时进行的 而类的加载是由ClassLoader来做的 所以开发者很难把握它的初始化时机
1.可能由于初始化太早造成资源浪费
2.如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。
一些其他的实现方式:
《Effective Java 1》 —— 静态内部类
这种写法非常巧妙:
对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。
《Effective Java 2》 —— 枚举
由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。
但是在需要继承的场景,它就不适用了
总结:任何一种方法都有他的用武之地 只是需要考虑一个度的问题 就需要开发者在开发中选择自己适合的单例就好了。