设计模式——单例模式

本贴最后更新于 462 天前,其中的信息可能已经时移世异

一、前言

由于工作原因,导致我断了一阵子的设计模式学习,今天我们来继续设计模式的学习。今天我们要了解的是单例模式——单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

二、单例模式详解

1、定义

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

这种模式的类,类负责创建自己的对象,同时确保只有单个对象被创建。并且提供一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

2、介绍

  • 意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点
  • 主要解决: 一个全局使用的类频繁地创建与销毁
  • 何时使用: 当您想控制实例数目,节省系统资源的时候
  • 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建
  • 关键代码: 构造函数是私有的

2.1、特点

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

2.2、应用实例

  1. 一个班级只有一个班主任
  2. Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行
  3. 一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件

2.3、优点

  1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
  2. 避免对资源的多重占用(比如写文件操作)

2.4、缺点

  1. 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化
  2. 也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难

2.5、使用场景

  1. 要求生产唯一序列号
  2. WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来
  3. 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等

3、UML 图

01 单例模式.jpg

示例

entity

public class SingleObject {

    //创建 SingleObject 的一个对象
    private static SingleObject instance = new SingleObject();

    //让构造函数为 private,这样该类就不会被实例化
    private SingleObject(){}

    //获取唯一可用的对象
    public static SingleObject getInstance(){
        return instance;
    }

    public void showMessage(){
        System.out.println("Hello World!");
    }
}

demo

public class SingletonPatternDemo {

    public static void main(String[] args) {
        //不合法的构造函数
        //编译时错误:构造函数 SingleObject() 是不可见的
        //SingleObject object = new SingleObject();

        //获取唯一可用的对象
        SingleObject object1 = SingleObject.getInstance();
        SingleObject object2 = SingleObject.getInstance();

        System.out.println(object1);
        System.out.println(object2);
    }
}

结果

02 单例模式.jpg

4、单例模式 VS 静态类

在知道了什么是单例模式后,我想小伙伴们一定会想到静态类,说:“既然只使用一个对象,为何不干脆使用静态类?”,这里我会将单例模式和静态类进行一个比较。

  1. 单例可以继承和被继承,方法可以被 override,而静态方法不可以
  2. 静态方法中产生的对象会在执行后被释放,进而被 GC 清理,不会一直存在于内存中
  3. 静态类会在第一次运行时初始化,单例模式可以有其他的选择,即可以延迟加载
  4. 基于 2, 3 条,由于单例对象往往存在于 DAO 层(例如 sessionFactory),如果反复的初始化和释放,则会占用很多资源,而使用单例模式将其常驻于内存可以更加节约资源
  5. 静态方法有更高的访问效率
  6. 单例模式很容易被测试

5、单例模式的几种实现方式

5.1、饿汉式

public class HungrySingle {
    private final static HungrySingle instance = new HungrySingle();

    private HungrySingle() {
        System.out.println(Thread.currentThread().getName());
    }

    public static HungrySingle getInstance() {
        return instance;
    }
}

歩骤

  1. 构造器私有
  2. 创建内部对象
  3. 写一个静态公共方法

优点

  1. 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题
  2. 没有任何锁,执行效率高,用户体验比懒汉式单例模式更好

缺点

  1. 在类装载的时候就完成实例化,没有达到 Lazy Loading(延迟加载) 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  2. 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用 getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果

建议

  1. 适用于单例模式较少的场景
  2. 如果我们在程序启动后,一定会加载到类,那么用饿汉模式实现的单例简单又实用
  3. 如果我们是写一些工具类,则优先考虑使用懒汉模式,可以避免提前被加载到内存中,占用系统资源

5.2、懒汉式

懒汉式(普通,线程不安全)
public class UnsafeLazySingle {
    private static UnsafeLazySingle instance;

    private UnsafeLazySingle() {
        System.out.println(Thread.currentThread().getName());
    }

    public static UnsafeLazySingle getInstance() {
        if (null == instance) {
            instance = new UnsafeLazySingle();
        }
        return instance;
    }
}

优点

  1. 起到了 Lazy Loading(延迟加载)的效果,但是只能在单线程下使用

缺点

  1. 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式

建议

  1. 多线程不能用
懒汉式(同步方法,线程安全)
class LazySingleF{
    private static LazySingleF instance;

    private LazySingleF(){
        System.out.println(Thread.currentThread().getName());
    }

    public static synchronized LazySingleF getInstance() {
        if (instance == null) {
            instance = new LazySingleF();
        }
        return instance;
    }
}

优点

  1. 解决了线程安全问题

缺点

  1. 效率太低了,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了

建议

  1. 在实际开发中,不推荐使用这种方式
懒汉式(同步代码块,线程不安全)
class LazySingleB{
    private static LazySingleB instance;

    private LazySingleB() {
        System.out.println(Thread.currentThread().getName());
    }

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

缺点

  1. 不能线程同步。假如一个线程进入了 if (instance == null),还未来得及执行,另一个线程也通过了这个判断语句,会产生多个实例
  2. 效率很低

建议

  1. 多线程不能使用
懒汉式(双重检查,线程安全)(Double Check Lock(DCL))
class LazySingleD{
    private volatile static LazySingleD instance;

    private LazySingleD() {
        System.out.println(Thread.currentThread().getName());
    }

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

优点

  1. 线程安全;延迟加载;效率较高
  2. Double-Check Lock 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步。

缺点

  1. 相比单锁而言,双重检查锁性能上虽然有提升,但是依旧用到了 synchronized 关键字总归要上锁,对程序性能还是存在一定的性能影响。注意里面 volatile 的使用!!!

建议

  1. 在实际开发中,推荐使用这种单例设计模式

注意

由于 jvm 存在乱序执行功能,DCL 也会出现线程不安全的情况。具体分析如下:

INSTANCE  = new SingleTon();

这个步骤,其实在 jvm 里面的执行分为三步:

  1. 在堆内存开辟内存空间
  2. 在堆内存中实例化 SingleTon 里面的各个参数
  3. 把对象指向堆内存空间

由于 jvm 存在乱序执行功能,所以可能在 2 还没执行时就先执行了 3,如果此时再被切换到线程 B 上,由于执行了 3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的 DCL 失效问题。

不过在 JDK1.5 之后,官方也发现了这个问题,故而具体化了 volatile,即在 JDK1.6 及以后,只要定义为

private volatile static LazySingleD instance;

就可解决 DCL 失效问题。volatile 的内存栅栏功能,告知编译器的在标记的变量前后不使用优化功能,禁止指令重排序

5.3、内部静态类

class InnerClass {
    private InnerClass() {
        System.out.println(Thread.currentThread().getName());
    }
    private static class InnerClassHolder {
        private static InnerClass instance = new InnerClass();
    }
    public static InnerClass getInstance() {
        return InnerClassHolder.instance;
    }
}

优点

  1. 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 INSTANCE,故而不占内存
  2. 当 InnerClass 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance()方法第一次被调用时,才会去初始化 INSTANCE,第一次调用 getInstance()方法会导致虚拟机加载 SingleTonHoler 类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

缺点

  1. 静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去

建议

  1. 屏蔽饿汉式单例模式的内存浪费问题和双重检查锁中 synchronized 的性能问题,同时考虑避免因为反射破坏单例问题,相对而言性能最好!
补充知识——类加载

JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化:

  1. 遇到 new、getstatic、setstatic 或者 invokestatic 这 4 个字节码指令时,对应的 java 代码场景为:

    new一个关键字或者一个实例化对象时
    
    读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)
    
    调用一个类的静态方法时。
    
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。

  3. 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()方法的类),虚拟机会先初始化这个类。

  5. 当使用 JDK 1.7 等动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上是类的主动引用。除此之外,所有引用类都不会对类进行初始化,称为被动引用。

静态内部类就属于被动引用的行列,为什么?

我们再回头看下 getInstance()方法,调用的是 InnerClassHolder.instance,取的是 InnerClassHolder 里的 instance 对象,跟上面那个 DCL 方法不同的是,getInstance()方法并没有多次去 new 对象,故不管多少个线程去调用 getInstance()方法,取的都是同一个 instance 对象,而不用去重新创建。

当 getInstance()方法被调用时,InnerClassHolder 才在 InnerClass 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 instance 也真正被创建,然后再被 getInstance()方法返回出去,这点同饿汉模式。

那么 instance 在创建过程中又是如何保证线程安全的呢?

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。

可以看出 INSTANCE 在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

5.4、枚举

enum Enums {
    /**
     * 实例对象
     */
    INSTANCE;
    /**
     * 两数相加之和方法
     * @param a
     * @param b
     * @return
     */
    public int add(int a, int b)
    {
        return a + b;
    }
}

优点

  1. 不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化
  2. 直接通过 Enums.INSTANCE.add()的方式调用即可。方便、简洁又安全

三、总结

以上就是我个人关于 设计模式——单例模式 的一些笔记,如果有什么问题,可以将问题发我邮箱 luodiab@126.com ,欢迎各位的意见。

四、参考文章

单例模式的使用总结

java 单例模式——详解 JAVA 单例模式及 8 种实现方式

单例模式

Java 单例模式怎么用?看这篇就够了

Java 单例模式,看这一篇就够了

Java 设计模式(一)-单例模式

  • 设计模式

    设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

    200 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...