面试
单例模式,主要作用是保证在Java程序中,某个类只有一个实例存在,在Java中一些管理器和控制器就被设计成单例模式,在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。
单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间;能够避免由于操作多个实例导致的逻辑错误。如果一个对象有可能贯穿整个应用程序,而且起到了全局统一管理控制的作用,那么单例模式也许是一个值得考虑的选择。
单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、双重校验锁单例。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
重点来了,当你自己写单例的时候,记住三点:私有的对象声明;私有的对象构造器;公有的获取实例方法。在你面试的时候,不管面试官让你口述或者写出来,你把这三点表述出来肯定就是一个单例。当然这仅仅是一个非常简单的单例,是一个单例的基础框架,面试官肯定会让你深入说明,而更深层的东西无外乎两点:线程安全和延迟加载。根据延迟加载,单例可划分为懒汉式单例和饿汉式单例。在讲解过程中我会把线程安全穿插进去。
饿汉式:
饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
1. publicclass Singleton{
2. private static Singleton instance = new Singleton(); //私有的对象声明
3. private Singleton(){} //私有的对象构造器
4. public static Singleton newInstance(){ //公有的获取实例方法
5. return instance;
6. }
7. }
懒汉式:
在需要的时候才去创建的,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象,致命的是在多线程不能正常工作。
1. publicclass Singleton{
2. private static Singleton instance = null; //私有的对象声明
3. private Singleton(){} //私有的对象构造器
4. public static Singleton newInstance(){ //公有的获取实例方法
5. if(null == instance){
6. instance = new Singleton();
7. }
8. return instance;
9. }
10. }
这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,即在获取实例方法的时候,添加同步锁synchronized ,此时该懒汉式单例为线程安全的,遗憾的是,效率很低,99%情况下不需要同步。
1. publicclass Singleton{
2. private static Singleton instance = null;
3. private Singleton(){}
4. public static synchronized Singleton newInstance(){
5. if(null == instance){
6. instance = new Singleton();
7. }
8. return instance;
9. }
10. }
双重校验锁:
加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。
1. publicclass Singleton {
2. private volatile static Singleton instance = null;
3. private Singleton(){}
4. public static Singleton getInstance() {
5. if (instance == null) {
6. synchronized (Singleton.class) {
7. if (instance == null) {
8. instance = new Singleton();
9. }
10. }
11. }
12. return instance;
13. }
14. }
将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。
静态内部类:
实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:
1. publicclass Singleton {
2.
3. /* 私有构造方法,防止被实例化 */
4. private Singleton() {
5. }
6.
7. /* 此处使用一个内部类来维护单例 */
8. privatestaticclass SingletonFactory {
9. privatestatic Singleton instance = new Singleton();
10. }
11.
12. /* 获取实例 */
13. publicstatic Singleton getInstance() {
14. return SingletonFactory.instance;
15. }
16. }
其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:
1. publicclass SingletonTest {
2.
3. private static SingletonTest instance = null;
4.
5. private SingletonTest() {
6. }
7.
8. private static synchronized void syncInit() {
9. if (instance == null) {
10. instance = new SingletonTest();
11. }
12. }
13.
14. public static SingletonTest getInstance() {
15. if (instance == null) {
16. syncInit();
17. }
18. return instance;
19. }
20. }
当然为了保证序列化时,前后对象一致,以上四种单例都可以添加一个方法。
1. /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
2. public Object readResolve() {
3. return instance;
4. }
枚举:
1. public enum Singleton{
2. instance;
3. public void whateverMethod(){}
4. }
上面提到的四种实现单例的方式都有共同的缺点:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。
总结
本文总结了五种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用双重校验锁和静态内部类方式实现单例模式。