飞道的博客

单例模式的5种实现方式(懒汉模式、饿汉模式、双重锁模式、静态内部类模式、枚举模式)

479人阅读  评论(0)


参考文章:
【1】http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
【2】https://www.cnblogs.com/xuwendong/p/9633985.html


学习内容:

一、概念

设计模式(Design pattern)
是一套被反复使用的代码设计经验总结,专门用于解决特定场景的需求。它提供了在软件开发过程中面临的一些问题的最佳解决方案,使用设计模式是为了提高代码的可重用性、让代码通俗易懂,增加代码可靠性。

单例设计模式(singleton)
1)作用:最常用、最简单的设计模式,单例的编写有很多种写法。
2)目的:保证在整个应用中某一个类有且只有一个实例(一个类在堆内存只存在一个对象)。

二、单例模式写法

2.1 饿汉式

  • 概念
    饿汉式,从名字上也很好理解,就是 “比较勤”,实例在初始化的时候就已经建好了,不管你有没有用到,都先建好了再说
  • 实现
    [1] 必须在该类中,自己先创建出一个对象
    [2] 私有化自身的构造器,防止外界通过构造器创建新的工具类对象
    [3] 向外暴露一个公共的静态方法用于返回自身的对象
// 单例模式(饿汉式)
public class Singleton1 {
   
    // [1]私有化构造方法
    private Singleton1() {
   
    }
    
    // [2] 事先创建好当前类的一个私有静态实例对象
    private static Singleton1 instance = new Singleton1();
    
    // [3] 提供一个公共静态方法(用于统一的外界访问方式),返回事先创建好的实例
    public static Singleton1 getSingleton() {
   
        return instance;
    }
}

注意:我们知道,类加载的方式是按需加载,且加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
好处:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
坏处:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。 如果从始至终从未使用过这个实例,则会造成内存的浪费。

2.2 懒汉式

  • 概念
    懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则返回,没有则新建。有线程安全和线程不安全两种写法,区别就是synchronized关键字。
  • 实现方法一(线程不安全,教科书一般是这样写懒汉式)
    [1] 必须在该类中,自己先创建出一个对象
    [2] 私有化自身的构造器,防止外界通过构造器创建新的工具类对象
    [3] 向外暴露一个公共的静态方法用于返回自身的对象
// 单例模式(懒汉式)
public class Singleton2 {
   
    // [1]私有化构造方法。
    private Singleton2() {
   
    }

    // [2] 事先创建好当前类的一个私有静态对象
    private static Singleton2 instance = null;

    // [3] 提供一个公共静态方法(用于统一的外界访问方式),返回事先创建好的实例
    public static Singleton2 getInstance() {
   
    	// 被动创建,在真正需要使用时才去创建
        if (null == instance) {
   
            instance= new Singleton2();
        }
        return instance;
    }
}

注意:我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
优缺点:这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

  • 实现方法二(线程安全)
    为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。
// 单例模式(懒汉式)
public class Singleton2 {
   
    // [1]私有化构造方法。
    private Singleton2() {
   
    }

    // [2] 事先创建好当前类的一个私有静态对象
    private static Singleton2 instance = null;

    // [3] 提供一个公共静态方法(用于统一的外界访问方式),返回事先创建好的实例
    public static synchronized Singleton2 getInstance() {
   
    	// 被动创建,在真正需要使用时才去创建
        if (null == instance) {
   
            instance= new Singleton2();
        }
        return instance;
    }
}

注意:虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这就引出了双重检验锁。

2.3 双重加锁机制

  • 概念
    双检锁,又叫双重校验锁,综合了懒汉式和饿汉式两者的优缺点整合而成。看上面代码实现中,特点是在synchronized关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
// 单例模式(懒汉式)
public class Singleton3 {
   
    private static Singleton3 instance;
    private Singleton3 (){
   }

    public static Singleton3 getSingleton() {
   
        if (instance == null) {
                            
            synchronized (Singleton3.class) {
   
                if (instance == null) {
          
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

注意:Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次 if (singleton == null) 检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接return实例化对象。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
使用双重检测同步延迟加载去创建单例的做法是一个非常优秀的做法,其不但保证了单例,而且切实提高了程序运行效率
优点:线程安全;延迟加载;效率较高。
缺点:这段代码看起来很完美,很可惜,它是有问题。主要在于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 就可以了。
volatile作用:
[1] Java提供了volatile关键字来保证可见性;
[2] 保证有序性,代码为【context = loadContext();inited = true;】;
[3] 提供double check。

解决后的代码为:

// 单例模式(懒汉式)
public class Singleton3 {
   
    private volatile static Singleton3 instance;
    private Singleton3 (){
   }

    public static Singleton3 getSingleton() {
   
        if (instance == null) {
                            
            synchronized (Singleton3.class) {
   
                if (instance == null) {
          
                    instance = new Singleton3();
                }
            }
        }
        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。

2.4 静态内部类

  • 概念
    我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。
public class Singleton4 {
     
    private static class SingletonHolder {
     
        private static final Singleton4 INSTANCE = new Singleton4();  
    }
    
    private Singleton4 (){
   }
    
    public static final Singleton4 getInstance() {
     
        return SingletonHolder.INSTANCE; 
    }  
}

注意:这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

2.5 枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{
   
	// 定义枚举常量
    INSTANCE;
}

使用: 我们可以通过EasySingleton.INSTANCE.工具方法() 的方式来访问实例,这比调用getInstance()方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。但是还是很少看到有人这样写,可能是因为不太熟悉吧。

三、小结

  • 单例总结
    一般来说,单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。
  • 选用方式
    就我个人而言,一般情况下直接使用饿汉式就好了,如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类,如果涉及到反序列化创建对象时会试着使用枚举的方式来实现单例。

总结:

以上就是单例模式的介绍了,代码仅供参考,欢迎讨论交流。


转载:https://blog.csdn.net/zzvar/article/details/117229703
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场