单例模式全形式

Posted

tags:

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

① 懒汉式:经典写法,线程不安全
 
public class Singleton {
    private static Singleton instance;    //私有静态自身类属性,由于静态方法只能访问静态成员,因此是静态的
    private Singleton (){}                //私有构造器
 
    public static Singleton getInstance() {  //给外界提供单例类对象的静态函数函数:由于使用第一次从单例类中获取单例对象的时候,没有初始化当前类对象,因此只能通过类调用静态方法的形式
     if (instance == null) {
        instance = new Singleton();
     }
     return instance;
    }
}
     
  之所以称为懒汉式,是因为当调用静态方法getInstance()的时候才会生成这个单例对象。
 
②懒汉式:线程安全的
 
public class Singleton {
    private static Singleton instance;    //私有静态自身类属性,由于静态方法只能访问静态成员,因此是静态的
    private Singleton (){}                //私有构造器
 
    public static synchronized Singleton getInstance() {  //给外界提供单例类对象的静态函数函数:由于使用第一次从单例类中获取单例对象的时候,没有初始化当前类对象,因此只能通过类调用静态方法的形式
      if (instance == null) {
        instance = new Singleton();
    }
    return instance;
    }
}
     
  可以说这个方式是为了处理懒汉式遇到的线程安全问题而想到的一个最偷懒的改进方式,这个方式确实避免了线程安全的问题,但是同时带来的确实是性能的问题,同一个函数,在同步操作之后性能可能是相差千百倍的。
 
③双重检查机制
 
public class Singleton {
    private static Singleton instance;    //私有静态自身类属性,由于静态方法只能访问静态成员,因此是静态的
    private Singleton (){}                //私有构造器
 
    public static synchronized Singleton getInstance() {  //给外界提供单例类对象的静态函数函数:由于使用第一次从单例类中获取单例对象的时候,没有初始化当前类对象,因此只能通过类调用静态方法的形式
        if (instance == null) {                        //Single Checked
             synchronized (Singleton.class) {
                 if (instance == null) {                //Double Checked 第二次检查的原因:有可能多个线程同时通过了第一个检查,但是遇到了同步操作,其中一个线程获取了操作权,因此必须再一次判断,否则后面的线程会再次创建一个对象
                     instance = new Singleton();
                 }
             }
         }
         return instance ;
    }  
}

 

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。
 
public class Singleton {
    private volatile static Singleton instance; //声明成 volatile
    private Singleton (){}

    public static Singleton getSingleton() {
        if (instance == null) {                       
            synchronized (Singleton.class) {
                if (instance == null) {     
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
 
}
     
  有些人认为使用 volatile 的原因是可见性,也就是可以保证线程在本地不会存有 instance 的副本,每次都是去主内存中读取。但其实是不对的。使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
    
     但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
 
     由于双重检查机制的复杂性,因此引入了下面的单例模式。
 
④饿汉式
 
public class Singleton{
    //类加载时就初始化
    private static final Singleton instance = new Singleton();   //使用final关键字保证instance对象只能初始化一次,其后不变
   
    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}
   
    这种方式既保证了线程安全,有保证了性能,同时代码也比较简单,但是,仍然存在的问题是: 譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
 
⑤静态内部类形式
 
public class Singleton { 
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton(); 
    } 
    private Singleton (){} 
    public static final Singleton getInstance() { 
        return SingletonHolder.INSTANCE;
    } 
}
     
  这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
 
⑥枚举类形式:
 
public enum EasySingleton{
    INSTANCE;   //默认是使用public static final修饰
}
 
     以上是使用枚举类型完成的单例模式,简单易用,我们可以通过EasySingleton.INSTANCE来访问实例,这比调用getInstance()方法简单多了,创建枚举默认就是线程安全的,下面介绍一下枚举类型:
 
class Season{
    //1.提供类的属性,声明为private final
    private final String seasonName;
    private final String seasonDesc;
    //2.声明为final的属性,在构造器中初始化。
    private Season(String seasonName,String seasonDesc){
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    //3.通过公共的方法来调用属性
    public String getSeasonName() {
        return seasonName;
    }
    public String getSeasonDesc() {
        return seasonDesc;
    }
    //4.创建枚举类的对象:将类的对象声明public static final
    public static final Season SPRING = new Season("spring", "春暖花开");    //如果只有一个对象,那么这个类就是一种单例模式的实现,和类似饿汉式,使用静态本类属性,一加载当前类就初始化,不同的是通过类名获取单一实例
    public static final Season SUMMER = new Season("summer", "夏日炎炎");
    public static final Season AUTUMN = new Season("autumn", "秋高气爽");
    public static final Season WINTER = new Season("winter", "白雪皑皑");
    @Override
    public String toString() {
        return "Season [seasonName=" + seasonName + ", seasonDesc="
                + seasonDesc + "]";
    }
    public void show(){
        System.out.println("这是一个季节");
    }
}

 

     通过以上代码可以使用Season.SPRING的形式获取当前类对象,如果仅仅在内部定义的了一个SPRING对象,那么这就是一种单例模式的实现。
 
总结:
     懒汉式、饿汉式、双重检查式都具备①一个静态的获取当前类对象的方法;②私有的构造函数;③一个私有的静态自身类属性
       内部类式:同样具备上述上个条件,区别在于使用了一个静态内部类,将外部类作为内部类的私有静态属性初始化,外部类的静态方法仅仅是获取通过静态内部类获取外部类对象传递。
       枚举类型:事实上没有使用enum关键字之前,枚举类型事实上是一种饿汉式的体现,与之不同的是枚举类型的自身类属性是public的,没有通过一个静态方法传递出去。

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

单例模式的5种形式

单例模式实例(全)

GOF23之一单例模式详细全解

静态类 VS 单例模式

单例模式的 8 种写法,非常全!

常用代码片段