单例模式---面试老生常谈的问题

我觉得Java入门其实并不难,随便在一家培训机构学习几个月也就能上手了,包括我自己也有过培训的经历,我现在发现很多东西都是停留在会用的阶段,

并不了解其原理,真正想要在软件这个行业有所成就,只是会用那还远远不够。所以我决定从基础起,把以前遇到的问题和还没搞得很清楚的技术点,一样一样的学明白。

今天我们先来聊聊面试总会问的设计模式之单例模式。

 

作用:  单例模式的作用是为了保证在Java程序中,某个类只有一个实例存在。一些管理器和控制器常被设计成单例模式。(SpringMVC的Controller就是单例的

    单例模式有很多好处,它能够避免实例的重复创建,减少每次创建对象的时间开销,还可以节约内存空间,能够避免由于操作多个实例而导致逻辑错误。

    如果一个对象能够贯穿整个应用程序,并起到一个全局统一管理的作用,那么这个时候,我们可以考虑使用单例模式。

 

    我这里主要总结单例模式的5种写法:饿汉模式,懒汉模式,双重校验锁模式,静态内部类实现单例,枚举实现单例

 

1、饿汉模式

  

 public class Singleton{
     private static Singleton instance = new Singleton();
     private Singleton(){}
     public static Singleton newInstance(){
         return instance;
     }
 }

   单例模式中类的构造函数必须是私有的,就是为防止其他类不能实例化此类(说白了就是在别的类中不能去new这个类的对象出来,就不允许别人创建,你只能通过我提供的公共方法来获取实例),饿汉模式在类加载的时间就会创建这个实例,实例在整个程序周期都存在。它的好处是不存在线程安全的问题,但是缺点也很明显,即使这个实例没有被用到,也会被创建出来,这样内存就被浪费了。(饿汉模式是以空间换时间,加载时占用空间来换取使用时的快捷)。

  这种方式适合单例占用内存比较小,并且在初始化的时候就会被使用到的情况。如果单例占用内存比较大,或者只在某个特定场景下才能使用,那么这个时候就要使用懒汉模式进行延迟加载了。

 

2、懒汉模式

 public class Singleton{
   private static Singleton instance = null;
   private Singleton(){}
   public static Singleton newInstance(){
      if(null == instance){
         instance = new Singleton();
        }
       return instance;
      }
  }

  懒汉模式是你在需要这个实例的时候才会去创建,之后再次使用这个实例的时候,就不会去重复创建了。它的好处就在于节省不必要的内存开销(以时间换空间,在第一次使用时速度会慢一些),它的缺点就在于会存在线程安全的问题。在多线程并发的情况下,会导致重复创建这个实例。可以在获取实例的方法上加锁解决线程同步的问题(synchronized关键字)。

 
 
3、双重校验锁模式
 
  双重校验锁也是基于懒汉模式的一种模式,虽说懒汉模式加了同步锁之后就没什么问题了,但是synchronized修饰的同步方法存在性能问题,会比一般的方法慢很多,在并发调用的情况下,会消耗较大的性能,所以才有了双重校验锁模式。
 public class Singleton {
    private static Singleton instance = null;
     private Singleton(){}
     public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                 if (instance == null) {
                    instance = new Singleton();
                 }
            }
         }
         return instance;
     }
 }

   可以看到在上面的同步代码块外面多了层instance为空的判断。由于单例对象是需要创建一次,如果后面再调用getInstance()方法时只需要返回单例对象就可以,

大部分情况下都不会执行到同步代码块,提高了程序的性能。但是在多线程并发的情况下,A,B两个线程同时执行了第一个instance不为空语句,两个线程都会认为当

前这个单例对象没有被创建,然后依次执行同步代码块,并分别创建了一个单例对象,为了解决这个问题,再同步代码块中又加了一个if (instance == null) 实例为空的判断。

  双重校验锁模式实现了延时加载,线程安全,挺高了效率,是否真的就万无一失呢?

  这里要提到java的指令重排优化,指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序,从而提高程序的运行效率。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

  这个问题的关键就在于指令重排优化的存在,会导致初始化单例类和将对象地址赋值给instance时的顺序是不正确的。在某个对象创建单例对象时,在构造方法被调用前,就为该对象分配了内存

空间,并将对象的字段设置为默认值,在分配内存的同时就可以将地址赋值给instance变量,然而实际上该对象可能没有被初始化。若紧接着另外一个线程来调用getInstanc()方法,就会获得不正确的

对象,程序就会出错。

  以上就是双重校验锁会失效的原因,还好在jdk1.5及以后增加了volatile关键字,volatile的一个语义就是禁止指令重排序优化,也就保证了instance变量被赋值时,对象已经被初始化。从而避免上面所说的问题。代码如下:

  

public class Singleton {
     private static volatile Singleton instance = null;
     private Singleton(){}
     public static Singleton getInstance() {
         if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                  }
            }
           }
          return instance;
       }
  }

 

4、静态内部类

 public class Singleton{
     private static class SingletonHolder{
         public static Singleton instance = new Singleton();
      }
     private Singleton(){}
     public static Singleton newInstance(){
         return SingletonHolder.instance;
     }
 }

  这种方式同样利用了类加载机制来保证只创建了一个instance实例,它与饿汉模式一样也是利用了类加载机制,因此不存在多线程并发的安全问题。不一样的是它在内部类中去创建对象实例,

这样的话只要在应用中不使用内部类,JVM就不会去加载这个单例类,也就不会去创建单例类的对象,从而实现了懒汉模式的延时加载。也就是说这种方式可以同时保证延时加载和线程安全。

 

5、枚举

 public enum Singleton{
     instance;
    public void whateverMethod(){}    
 }

  上面提到的四种实现单例的方法都有共同的缺点:

  1、需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。

  2、可以使用反射强行调用私有构造器。(如果要避免这种情况的发生,可以修改构造器,让他在创建第二个实例的时候抛出异常)

  而枚举很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候去创建新的对象。

  因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

  

  总结

    以上说到的是5种实现单例模式的方式,1,2两种并不是特别的完美,建议工作中使用3,4,5这三种

 

 

posted @ 2018-10-02 10:34  有个八块腹肌的梦想  阅读(307)  评论(0编辑  收藏  举报