设计模式---单例(Singleton)模式

设计模式中的单例模式,是最简单,也是最常用的一种设计模式.您真的了解吗?
饿汉式与懒汉式有什么区别,各有什么优缺点,您知道吗?
除了这两种写法,其实还可以通过枚举、静态内部类去实现,您知道吗?
想了解更多内容,请阅读下文:

单例: 确保一个类只有一个实例,并提供一个全局访问点.

一.单例的优缺点

1.优点

(1) 提供了对唯一实例的受控访问;
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;
(3) 可以根据实际情况需要,在单例模式的基础上扩展做出双例模式,多例模式;

2.缺点

(1) 单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
(2) 如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。

二.实现方式

单例模式的实现方式有多种,根据需求场景,可分为2大类、6种实现方式。具体如下(图片点击可放大查看):
在这里插入图片描述

(I)初始化时创建单例

1. 饿汉式

(1)这是 最简单的单例实现方式

(2)原理: 依赖 JVM类加载机制,保证单例只会被创建1次,即 线程安全。

  • JVM在类的初始化阶段(即 在Class被加载后、被线程使用前),会执行类的初始化
  • 在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化

(3)代码实现:

代码片1
public class Singleton {
 
        // 1. 创建私有变量 ourInstance(用以记录 Singleton 的唯一实例)
       //  2. 内部进行实例化
        private static Singleton instance = new Singleton();

        //3. 把类的构造方法私有化,不让外部调用构造方法实例化
        private Singleton() {

       //4. 定义公有方法提供该类的全局唯一访问点
       //5. 外部通过调用getInstance()方法来返回唯一的实例
        public static Singleton getInstance() {
             return instance;
        }
    }

(4)应用场景
除了初始化单例类时 即 创建单例外,继续延伸出来的是:单例对象 要求初始化速度快 & 占用内存小

2. 枚举类型

(1)根据枚举类型的特点,满足单例模式所需的 创建单例、线程安全、实现简洁的需求

(2)代码如下:

代码片2
public enum Singleton {
    //定义1个枚举的元素,即为单例类的1个实例
    INSTANCE;

    public void test() {
        //do something
    }
}

(3) 这是 最简洁、易用 的单例实现方式,借用《Effective Java》的话:
单元素的枚举类型已经成为实现 Singleton的最佳方法.

(II)按需、延迟创建单例

3. 懒汉式(基础实现)

(1)与 饿汉式 最大的区别是:单例创建的时机

  • 饿汉式:单例创建时机不可控,即类加载时 自动创建 单例
  • 懒汉式:单例创建时机可控,即有需要时,才 手动创建 单例

(2)代码实现:

代码片3
class Singleton {
    // 1. 类加载时,先不自动创建单例
   //  即,将单例的引用先赋值为 Null
    private static  Singleton ourInstance  = null;

    // 2. 构造函数 设置为 私有权限
    // 原因:禁止他人创建实例 
    private Singleton() {
    }
    
    // 3. 需要时才手动调用 newInstance() 创建 单例   
    public static  Singleton newInstance() {
    // 先判断单例是否为空,以避免重复创建
    if( ourInstance == null){
        ourInstance = new Singleton();
     }
     return ourInstance;
    }
}

(3)缺点
基础实现的懒汉式是线程不安全的,具体原因如下(图片点击可放大查看):
在这里插入图片描述
针对基础实现懒汉式的写法,有如下2种解决方案.继续往下看.

4. 同步锁(懒汉式的改进)

(1)原理
使用同步锁 synchronized锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建

  • 即,getInstance()方法块只能运行在1个线程中
  • 若该段代码已在1个线程中运行,另外1个线程试图运行该块代码,则 会被阻塞而一直等待
  • 而在这个线程安全的方法里我们实现了单例的创建,保证了多线程模式下 单例对象的唯一性

(2)代码如下:

// 写法1
class Singleton {
    // 1. 类加载时,先不自动创建单例
    //  即,将单例的引用先赋值为 Null
    private static  Singleton ourInstance  = null;
    
    // 2. 构造函数 设置为 私有权限
    // 原因:禁止他人创建实例 
    private Singleton() {
    }
    
    // 3. 加入同步锁
    public static synchronized Singleton getInstance(){
        // 先判断单例是否为空,以避免重复创建
        if ( ourInstance == null )
            ourInstance = new Singleton();
        return ourInstance;
   }
}


// 写法2
// 该写法的作用与上述写法作用相同,只是写法有所区别
class Singleton{ 

    private static Singleton instance = null;

    private Singleton(){
    }

    public static Singleton getInstance(){
        // 加入同步锁
        synchronized(Singleton.class) {
            if (instance == null)
                instance = new Singleton();
        }
        return instance;
    }
}

(3)缺点
每次访问都要进行线程同步(即 调用synchronized锁),造成过多的同步开销(加锁 = 耗时、耗能)。

PS: 实际上只需在第1次调用该方法时才需要同步,一旦单例创建成功后,就没必要进行同步。

5. 双重校验锁(懒汉式的改进)

(1)原理
在同步锁的基础上,添加1层 if判断:首先检查实例已经被创建了,如果尚未创建,"才"进行同步,若单例已创建,则不需再执行加锁操作就可获取实例(===>即:只有第一次会同步),从而提高性能。

(2)代码实现:

public class Singleton2 {
    
    private volatile static Singleton2 ourInstance = null;

    private Singleton2() {
    }

    public static Singleton2 newInstance() {
        // 加入双重校验锁
        // 校验锁1:第1个if
        if (ourInstance == null) {  // ①
            synchronized (Singleton2.class) { // ②
                // 校验锁2:第2个 if
                if (ourInstance == null) {
                    ourInstance = new Singleton2();
                }
            }
        }
        return ourInstance;
    }

    // 说明
    // 校验锁1:第1个if
    // 作用:若单例已创建,则直接返回已创建的单例,无需再执行加锁操作
    // 即直接跳到执行 return ourInstance

    // 校验锁2:第2个 if 
    // 作用:防止多次创建单例问题
    // 原理
    // 1. 线程A调用newInstance(),当运行到②位置时,此时线程B也调用了newInstance()
    // 2. 因线程A并没有执行instance = new Singleton();,此时instance仍为空,因此线程B能突破第1层 if 判断,运行到①位置等待synchronized中的A线程执行完毕
    // 3. 当线程A释放同步锁时,单例已创建,即instance已非空
    // 4. 此时线程B 从①开始执行到位置②。此时第2层 if 判断 = 为空(单例已创建),因此也不会创建多余的实例
}

其中第2行代码的java关键字volatile 简单说明一下:
volatile关键字确保,当ourInstance 变量被初始化成Singleton2 实例时,多个线程正确地处理ourInstance 变量。

当性能是你关注的重点,这种写法就可以大大帮你介绍newInstance的时间消耗。

(3)缺点:
没什么明显缺点,就写法有点复杂,容易出错。

6. 静态内部类

(1)原理
根据 静态内部类 的特性,同时解决了按需加载、线程安全的问题,同时实现简洁

  • 在静态内部类里创建单例,在装载该内部类时才会去创建单例。
  • 线程安全:类是由 JVM加载,而JVM只会加载1遍,保证只有1个单例。

(2)代码实现

public class Singleton3 {
    
    // 1. 创建静态内部类
    private static class SingleTonHoler{
        // 在静态内部类里创建单例
        private static Singleton3 INSTANCE = new Singleton3();
    }

    // 私有构造函数
    private Singleton3(){}

    // 延迟加载、按需创建
    public static Singleton3 getInstance(){
        return SingleTonHoler.INSTANCE;
    }
}

// 调用过程说明:
// 1. 外部调用类的getInstance 
// 2. 自动调用SingleTonHoler.INSTANCE
// 2.1 此时单例类Singleton3得到初始化
// 2.2 而该类在装载 & 被初始化时,会初始化它的静态域,从而创建单例;
// 2.3 由于是静态域,因此只会JVM只会加载1遍,Java虚拟机保证了线程安全性
// 3. 最终只创建1个单例

(3)缺点:
静态内部类有一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context这种参数,所以,我们创建单例时,可以在静态内部类与DCL模式里自己斟酌。

关于静态内部类创建单例模式,推荐大家阅读以下文章,可以更加深入的理解:
深入理解单例模式:静态内部类单例原理

三.总结:

本文主要对 单例模式 进行了全面介绍,包括原理 & 实现方式。对于实现方式,此处作出总结(图片点击可放大查看):
在这里插入图片描述

发布了81 篇原创文章 · 获赞 37 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/gaolh89/article/details/93235424
今日推荐