关于java:深入理解单例设计模式

34次阅读

共计 9045 个字符,预计需要花费 23 分钟才能阅读完成。

这是我参加更文挑战的第 2 天,流动详情查看:更文挑战

一、概述

单例模式是面试中常常会被问到的一个问题,网上有大量的文章介绍单例模式的实现,本文也是参考那些优良的文章来做一个总结,通过本人在学习过程中的了解进行记录,并补充欠缺一些内容,一方面坚固本人所学的内容,另一方面心愿能对其他同学提供一些帮忙。

本文次要从以下几个方面介绍单例模式:

  1. 单例模式是什么
  2. 单例模式的应用场景
  3. 单例模式的优缺点
  4. 单例模式的实现(重点)
  5. 总结

二、单例模式是什么

23 种设计模式能够分为三大类:创立型模式、行为型模式、结构型模式。单例模式属于创立型模式的一种,单例模式是最简略的设计模式之一:单例模式只波及一个类,确保在零碎中一个类只有一个实例,并提供一个全局拜访入口。许多时候整个零碎只须要领有一个全局对象,这样有利于咱们协调系统整体的行为。

三、单例模式的应用场景

1、日志类

日志类通常作为单例实现,并在所有应用程序组件中提供全局日志拜访点,而无需在每次执行日志操作时创建对象。

2、配置类

将配置类设计为单例实现,比方在某个服务器程序中,该服务器的配置信息寄存在一个文件中,这些配置数据由一个单例对象对立读取,而后服务过程中的其余对象再通过这个单例对象获取这些配置信息,这种形式简化了在简单环境下的配置管理。

3、工厂类

假如咱们设计了一个带有工厂的应用程序,以在多线程环境中生成带有 ID 的新对象(Acount、Customer、Site、Address 对象)。如果工厂在 2 个不同的线程中被实例化两次,那么 2 个不同的对象可能有 2 个重叠的 id。如果咱们将工厂实现为单例,咱们就能够防止这个问题,联合形象工厂或工厂办法和单例设计模式是一种常见的做法。

4、以共享模式拜访资源的类

比方网站的计数器,个别也是采纳单例模式实现,如果你存在多个计数器,每一个用户的拜访都刷新计数器的值,这样的话你的实计数的值是难以同步的。然而如果采纳单例模式实现就不会存在这样的问题,而且还能够防止线程平安问题。

5、在 Spring 中创立的 Bean 实例默认都是单例模式存在的。

实用场景:

  • 须要生成惟一序列的环境
  • 须要频繁实例化而后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又常常用到的对象。
  • 不便资源互相通信的环境

四、单例模式的优缺点

长处:

  • 在内存中只有一个对象,节俭内存空间;
  • 防止频繁的创立销毁对象,加重 GC 工作,同时能够进步性能;
  • 防止对共享资源的多重占用,简化拜访;
  • 为整个零碎提供一个全局拜访点。

毛病:

  • 不适用于变动频繁的对象;
  • 滥用单例将带来一些负面问题,如为了节俭资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而呈现连接池溢出;
  • 如果实例化的对象长时间不被利用,零碎会认为该对象是垃圾而被回收,这可能会导致对象状态的失落;

五、单例模式的实现(重点)

实现单例模式的步骤如下:

  1. 私有化构造方法,防止外部类通过 new 创建对象
  2. 定义一个公有的动态变量持有本人的类型
  3. 对外提供一个动态的公共办法来获取实例
  4. 如果实现了序列化接口须要保障反序列化不会从新创建对象

1、饿汉式,线程平安

饿汉式单例模式,顾名思义,类一加载就创建对象,这种形式比拟罕用,但容易产生垃圾对象,节约内存空间。

长处:线程平安,没有加锁,执行效率较高
毛病:不是懒加载,类加载时就初始化,节约内存空间

懒加载(lazy loading):应用的时候再创建对象

饿汉式单例是如何保障线程平安的呢?它是基于类加载机制防止了多线程的同步问题,然而如果类被不同的类加载器加载就会创立不同的实例。

代码实现,以及应用反射毁坏单例:

/**
 * 饿汉式单例测试
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){}
    // 2、定义一个动态变量指向本人类型
    private final static Singleton instance = new Singleton();
    // 3、对外提供一个公共的办法获取实例
    public static Singleton getInstance() {return instance;}

}

应用反射毁坏单例,代码如下:


public class Test {public static void main(String[] args) throws Exception{
        // 应用反射毁坏单例
        // 获取空参构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
        // 设置强制拜访
        declaredConstructor.setAccessible(true);
        // 创立实例
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println("反射创立的实例" + singleton);
        System.out.println("失常创立的实例" + Singleton.getInstance());
        System.out.println("失常创立的实例" + Singleton.getInstance());
    }
}

输入后果如下:

反射创立的实例 com.example.spring.demo.single.Singleton@6267c3bb
失常创立的实例 com.example.spring.demo.single.Singleton@533ddba
失常创立的实例 com.example.spring.demo.single.Singleton@533ddba

2、懒汉式,线程不平安

这种形式在单线程下应用没有问题,对于多线程是无奈保障单例的,这里列出来是为了和前面应用锁保障线程平安的单例做比照。

长处:懒加载

毛病:线程不平安

代码实现如下:


/**
 * 懒汉式单例,线程不平安
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){}
    // 2、定义一个动态变量指向本人类型
    private static Singleton instance;
    // 3、对外提供一个公共的办法获取实例
    public static Singleton getInstance() {
        // 判断为 null 的时候再创建对象
        if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

应用多线程毁坏单例,测试代码如下:

public class Test {public static void main(String[] args) {for (int i = 0; i < 3; i++) {new Thread(() -> {System.out.println("多线程创立的单例:" + Singleton.getInstance());
            }).start();}
    }
}

输入后果如下:

多线程创立的单例:com.example.spring.demo.single.Singleton@18396bd5
多线程创立的单例:com.example.spring.demo.single.Singleton@7f23db98
多线程创立的单例:com.example.spring.demo.single.Singleton@5000d44

3、懒汉式,线程平安

懒汉式单例如何保障线程平安呢?通过 synchronized 关键字加锁保障线程平安,synchronized 能够增加在办法下面,也能够增加在代码块下面,这里演示增加在办法下面,存在的问题是 每一次调用 getInstance 获取实例时都须要加锁和开释锁,这样是十分影响性能的。

长处:懒加载,线程平安

毛病:效率较低

代码实现如下:

/**
 * 懒汉式单例,办法下面增加 synchronized 保障线程平安
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton  {
    // 1、私有化构造方法
    private Singleton(){}
    // 2、定义一个动态变量指向本人类型
    private static Singleton instance;
    // 3、对外提供一个公共的办法获取实例
    public synchronized static Singleton getInstance() {if (instance == null) {instance = new Singleton();
        }
        return instance;
    }
}

4、双重查看锁(DCL,即 double-checked locking)

实现代码如下:


/**
 * 双重查看锁(DCL,即 double-checked locking)*
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造方法
    private Singleton() {}

    // 2、定义一个动态变量指向本人类型
    private volatile static Singleton instance;

    // 3、对外提供一个公共的办法获取实例
    public synchronized static Singleton getInstance() {
        // 第一重查看是否为 null
        if (instance == null) {
            // 应用 synchronized 加锁
            synchronized (Singleton.class) {
                // 第二重查看是否为 null
                if (instance == null) {
                    // new 关键字创建对象不是原子操作
                    instance = new Singleton();}
            }
        }
        return instance;
    }
}

长处:懒加载,线程平安,效率较高

毛病:实现较简单

这里的双重查看是指两次非空判断,锁指的是 synchronized 加锁,为什么要进行双重判断,其实很简略,第一重判断,如果实例曾经存在,那么就不再须要进行同步操作,而是间接返回这个实例,如果没有创立,才会进入同步块,同步块的目标与之前雷同,目标是为了避免有多个线程同时调用时,导致生成多个实例,有了同步块,每次只能有一个线程调用拜访同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创立,之后的所有调用都不会进入同步块,间接在第一重判断就返回了单例。

对于外部的第二重空判断的作用,当多个线程一起达到锁地位时,进行锁竞争,其中一个线程获取锁,如果是第一次进入则为 null,会进行单例对象的创立,实现后开释锁,其余线程获取锁后就会被空判断拦挡,间接返回已创立的单例对象。

其中最要害的一个点就是 volatile 关键字的应用,对于 volatile 的具体介绍能够间接搜寻 volatile 关键字即可,有很多写的十分好的文章,这里不做具体介绍,简略阐明一下,双重查看锁中应用 volatile 的两个重要个性:可见性、禁止指令重排序

这里为什么要应用 volatile

这是因为 new 关键字创建对象不是原子操作,创立一个对象会经验上面的步骤:

  1. 在堆内存开拓内存空间
  2. 调用构造方法,初始化对象
  3. 援用变量指向堆内存空间

对应字节码指令如下:

为了进步性能,编译器和处理器经常会对既定的代码执行程序进行指令重排序,从源码到最终执行指令会经验如下流程:

graph LR
A[源码] -->B([编译器优化重排序])-->C([指令级并行重排序])-->D([内存零碎重排序])-->E[最终执行指令序列]

所以通过指令重排序之后,创建对象的执行程序可能为 1 2 3 或者 1 3 2 ,因而当某个线程在乱序运行 1 3 2 指令的时候,援用变量指向堆内存空间,这个对象不为 null,然而没有初始化,其余线程有可能这个时候进入了 getInstance 的第一个 if(instance == null) 判断不为 nulll,导致谬误应用了没有初始化的非 null 实例,这样的话就会出现异常,这个就是驰名的 DCL 生效问题。

当咱们在援用变量下面增加 volatile 关键字当前,会通过在创建对象指令的前后增加内存屏障来禁止指令重排序,就能够防止这个问题,而且对 volatile 润饰的变量的批改对其余任何线程都是可见的。

5、动态外部类

代码实现如下:


/**
 * 动态外部类实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public class Singleton {
    // 1、私有化构造方法
    private Singleton() {}
    
    // 2、对外提供获取实例的公共办法
    public static Singleton getInstance() {return InnerClass.INSTANCE;}

    // 定义动态外部类
    private static class InnerClass{private final static Singleton INSTANCE = new Singleton();
    }

}

长处:懒加载,线程平安,效率较高,实现简略

动态外部类单例是如何实现懒加载的呢?首先,咱们先理解下类的加载机会。

虚拟机标准要求有且只有 5 种状况必须立刻对类进行初始化(加载、验证、筹备须要在此之前开始):

  1. 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时。生成这 4 条指令最常见的 Java 代码场景是:应用 new 关键字实例化对象的时候、读取或设置一个类的动态字段(final 润饰除外,被 final 润饰的动态字段是常量,已在编译期把后果放入常量池)的时候,以及调用一个类的静态方法的时候。
  2. 应用 java.lang.reflect 包办法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则须要先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(蕴含 main()的那个类),虚构机会先初始化这个主类。
  5. 当应用 JDK 1.7 的动静语言反对时,如果一个 java.lang.invoke.MethodHandle 实例最初的解析后果是 REF_getStaticREF_putStaticREF_invokeStatic 的办法句柄,则须要先触发这个办法句柄所对应的类的初始化。

这 5 种状况被称为是类的被动援用,留神,这里《虚拟机标准》中应用的限定词是 “有且仅有“,那么,除此之外的所有援用类都不会对类进行初始化,称为被动援用。动态外部类就属于被动援用的状况。

当 getInstance()办法被调用时,SingleTonHoler 才在 SingleTon 的运行时常量池里,把符号援用替换为间接援用,这时动态对象 INSTANCE 也真正被创立,而后再被 getInstance()办法返回进来,这点同饿汉模式。

那么 INSTANCE 在创立过程中又是如何保障线程平安的呢?在《深刻了解 JAVA 虚拟机》中,有这么一句话:

 虚构机会保障一个类的 <clinit>() 办法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 办法,其余线程都须要阻塞期待,直到流动线程执行 <clinit>() 办法结束。如果在一个类的 <clinit>() 办法中有耗时很长的操作,就可能造成多个过程阻塞 ( 须要留神的是,其余线程尽管会被阻塞,但如果执行 <clinit>() 办法后,其余线程唤醒之后不会再次进入 <clinit>() 办法。同一个加载器下,一个类型只会初始化一次。),在理论利用中,这种阻塞往往是很荫蔽的。

从下面的剖析能够看出 INSTANCE 在创立过程中是线程平安的,所以说动态外部类模式的单例可保障线程平安,也能保障单例的唯一性,同时也提早了单例的实例化。

6、枚举单例

代码实现如下:

/**
 * 枚举实现单例
 *
 * @className: Singleton
 * @date: 2021/6/7 14:32
 */
public enum Singleton {
    INSTANCE;
    public void doSomething(String str) {System.out.println(str);
    }
}

长处:简略,高效,线程平安,能够防止通过反射毁坏枚举单例

枚举在 java 中与一般类一样,都能领有字段与办法,而且枚举实例创立是线程平安的,在任何状况下,它都是一个单例,能够间接通过如下形式调用获取实例:

Singleton singleton = Singleton.INSTANCE;

应用上面的命令反编译枚举类

javap Singleton.class

失去如下内容

Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
  public static final com.spring.demo.singleton.Singleton INSTANCE;
  public static com.spring.demo.singleton.Singleton[] values();
  public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
  public void doSomething(java.lang.String);
  static {};}

从枚举的反编译后果能够看到,INSTANCE 被 static final 润饰,所以能够通过类名间接调用,并且创建对象的实例是在动态代码块中创立的,因为 static 类型的属性会在类被加载之后被初始化,当一个 Java 类第一次被真正应用到的时候动态资源被初始化、Java 类的加载和初始化过程都是线程平安的,所以创立一个 enum 类型是线程平安的。

通过反射毁坏枚举,实现代码如下:

public class Test {public static void main(String[] args) throws Exception {
        Singleton singleton = Singleton.INSTANCE;
        singleton.doSomething("hello enum");

        // 尝试应用反射毁坏单例
        // 枚举类没有空参构造方法,反编译后能够看到枚举有一个两个参数的构造方法
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
        // 设置强制拜访
        declaredConstructor.setAccessible(true);
        // 创立实例,这里会报错,因为无奈通过反射创立枚举的实例
        Singleton enumSingleton = declaredConstructor.newInstance();
        System.out.println(enumSingleton);
    }
}

运行后果报如下谬误:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
    at com.spring.demo.singleton.Test.main(Test.java:24)

查看反射创立实例的 newInstance() 办法,有如下判断:

所以无奈通过反射创立枚举的实例。

六、总结

在 java 中,如果一个 Singleton 类实现了 java.io.Serializable 接口,当这个 singleton 被屡次序列化而后反序列化时,就会创立多个 Singleton 类的实例。为了防止这种状况,应该实现 readResolve 办法。请参阅 javadocs 中的 Serializable () 和 readResolve Method ()。


public class Singleton implements Serializable {
    // 1、私有化构造方法
    private Singleton() {}

    // 2、对外提供获取实例的公共办法
    public static Singleton getInstance() {return InnerClass.instance;}

    // 定义动态外部类
    private static class InnerClass{private final static Singleton instance = new Singleton();
    }


    // 对象被反序列化之后,这个办法立刻被调用,咱们重写这个办法返回单例对象.
    protected Object readResolve() {return getInstance();
    }
}

应用单例设计模式须要留神的点:

  • 多线程 - 在多线程应用程序中必须应用单例时,应特地小心。
  • 序列化 - 当单例实现 Serializable 接口时,他们必须实现 readResolve 办法以防止有 2 个不同的对象
  • 类加载器 - 如果 Singleton 类由 2 个不同的类加载器加载,咱们将有 2 个不同的类,每个类加载一个。
  • 由类名示意的全局拜访点 - 应用类名获取单例实例。这是一种拜访它的简略办法,但它不是很灵便。如果咱们须要替换 Sigleton 类,代码中的所有援用都应该相应地扭转。

本文简略介绍了单例设计模式的几种实现形式,除了枚举单例,其余的所有实现都能够通过反射毁坏单例模式,在《effective java》中举荐枚举实现单例模式,在理论场景中应用哪一种单例实现,须要依据本人的状况抉择,适宜以后场景的才是比拟好的形式。

参考文章

https://blog.csdn.net/mnb6548…

https://www.oodesign.com/sing…

正文完
 0