单例模式详解——步步改进

Posted 刘小绪同学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式详解——步步改进相关的知识,希望对你有一定的参考价值。

单例设计模式简介

    可以使用单例设计模式的类都有一个共性,那就是这个类没有自己的状态。换句话说,这些类不论你实例化多少个都是一样的;更为重要的一点是,这个类如果有两个或两个以上的实例话,程序可能会产生错误活这与现实相违背的逻辑。Spring中的bean默认就是单例的。

    这样的话,如果我们不将这个类控制成单例结构,应用中就会存在很多一模一样的类实例。这不仅浪费内存资源,而且容易产生错误,因此使用单例设计模式就是尽可能的节省内存空间,减少不必要的GC消耗,并且使应用可以正常工作。

    一个类是否可以做成单例,最容易区别的地方就在于,这些类在应用中如果有两个或两个以上的实例会引起错误;即这个类在整个应用中,同一时刻有且只有一种状态。

    一般实践当中,有很多应用级别的资源会被做成单例的,比如配置文件信息。逻辑上来讲,整个应用在同一时刻只能有一个,如果有多个可能不会引起程序级别的错误。但是当我们试图改变配置文件的时候,问题就来了。两种选择:第一种就是把所有的实例全部变成一模一样饿;第二种就是等着出问题。

单例设计模式实现方式

    下面是单例设计模式不同的实现方式,也是单例设计模式逐步改进的示例。

普通版
public class 单例设计模式普通版 {    
   private static 单例设计模式普通版 SINGLETONA;
       
   private 单例设计模式普通版(){}    //饿汉式    
   //懒汉式是不管你要不要,我先造一个    //饿汉式是你要的时候我才造    public static 单例设计模式普通版 getInstance(){        
       if(SINGLETONA == null){            SINGLETONA = new 单例设计模式普通版();        }        
       return SINGLETONA;    } }

    这是再没有考虑并发的情况下标准的单例模式构造方式,通过以下几点来限制取到的实例是唯一的。

  • 静态实例,带有static关键字的属性在每一个类中都是唯一的。

  • 限制客户端再构造实例,将构造方法私有化,此为保证单例的重要一步。

  • 给一个公共的获取实例的静态方法,这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。

  • 判断只有持有的静态实例为null时才调用构造方法创造一个实例,否则就直接返回。

    上面的构造方式,只能在非并发情况下使用,如果是并发环境,就会发生错误。

同步版
public class 单例设计模式同步版 {    
   private static 单例设计模式同步版 SYNCHRONIZEDSINGLETON;
       
   private 单例设计模式同步版(){};    
   
   public static synchronized 单例设计模式同步版 getInstance(){        
       if(SYNCHRONIZEDSINGLETON == null){            SYNCHRONIZEDSINGLETON = new 单例设计模式同步版();        }        
       return SYNCHRONIZEDSINGLETON;    } }

    上面的写法,就是将获取实例的方法同步,这样在一个线程访问这个方法时,其他线程都要处于挂起等待状态。避免了上面并发访问创造出多个实例的危险,但是也造成了很多无谓的等待。

    因此我们可以继续优化。其实需要同步的地方只是需要发生在单例的实例还没有创建的时候,在实例已经创建好了的时候,获取实例的方法就没有必要再进行同步控制了。就可以改为双重加锁,像下面这样。

双重加锁版
public class 单例设计模式同步改进版 {    

   private static 单例设计模式同步改进版 SYNCHRONIZEDSINGLETON;  
     
   private 单例设计模式同步改进版() {}    
   
   public static 单例设计模式同步改进版 getInstance(){        
       if(SYNCHRONIZEDSINGLETON == null){            
           synchronized (单例设计模式同步改进版.class) {                
           if(SYNCHRONIZEDSINGLETON == null){                    SYNCHRONIZEDSINGLETON = new 单例设计模式同步改进版();                }            }        }        
       return SYNCHRONIZEDSINGLETON;    } }

    这种方法比上面的同步整个方法要好的多,因为只是在当前实例为null时才进行同步,否则直接返回,这样就节省了很多无谓的线程等待时间。需要注意的是,在同步块中再次进行了null值的判断以此来保证只创建一个实例,为什么要这样做呢?

    假设去掉同步块中的null判断,有一个种情况会导致创建多个实例。假设A线程和B线程都在同步块外面判断了null,结果A线程首先获得了线程锁,进入了同步块,然后A线程会创建一个实例,再退出同步块,返回自己创建的实例;此时B线程获得线程锁,也进入了同步块,前面A线程已经创建了实例,B线程现在就应该直接返回,但是因为同步块中没有判断是否为null,直接就是一条创建实例的语句,所以B线程也会创建一个实例返回,这就造成了创建多个实例的情况。

    如果深入到JVM中考虑,上面的代码依旧可能出现问题。因为JVM在创建实例的时候,是分好几步去进行的。也就是说,创建对象的实例不是一个原子操作,在某些JVM中没有问题,但是在有些情况下就会出错。

    我们无法在语言级别完全避免错误的发生,则只有将任务交给JVM,所以有一种比较标准的单例模式,采用静态内部类实现。因为一个类的静态属性只会在第一次加载类的时候被初始化,这是JVM保证的,一次无需担心并发访问的问题。在初始化进行一半时,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以依旧是单例的。

静态内部类版
public class 单例设计模式最终版 {
 
   private 单例设计模式最终版() {}    
   public static 单例设计模式最终版 getInstance(){        
       return SingletonInstance.SINGLETONINSTANCE;    }
 
   //采用静态内部类实现    private static class SingletonInstance{
           static 单例设计模式最终版 SINGLETONINSTANCE = new 单例设计模式最终版();    } }

    上面的写法就是最好的单例模式,其特点如下:

  1. 在不考虑反射强行突破访问限制的情况下,最多只有一个实例。

  2. 保证了并发访问的情况下,不会发生由于并发而产生多个实例。

  3. 保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。

    (^_^)至此,单例模式已经完成了。

————END————



以上是关于单例模式详解——步步改进的主要内容,如果未能解决你的问题,请参考以下文章

单例模式--饿汉懒汉多线程以及多线程下改进

Java设计模式--单例模式(代码详解懒汉饿汉模式)

单例模式的持续改进

设计模式之单例模式详解和应用

单例模式详解

单例模式详解