> 文章列表 > 【设计模式】Java中的单例模式

【设计模式】Java中的单例模式

【设计模式】Java中的单例模式

文章目录

  • 一、前言
  • 二、什么是单例模式?
  • 三、单例模式的核心特点
  • 四、单例模式的多种实现方式
    • 1、饿汉式
    • 2、懒汉式
    • 3、静态内部类
    • 4、枚举
  • 五、如何解决序列化反序列化导致单例模式失效问题
  • 六、总结

一、前言

本文旨在通过由浅入深的方式带大家深入的了解各种单例模式,接下来我会先简单介绍一下单例模式,给出相应单例类的代码,然后通过一些问题来介绍各个单例模式需要注意的地方,还会给出相应的测试代码。

二、什么是单例模式?

单例模式(Singleton Pattern)属于创建型设计模式,单例指的就是单实例,我们通常都是使用 new 来创建对象的,直接 new 的话创建出来的是不同的对象,如果我们想要确保一个类只能有一个对象,那么我们就可以采用单例模式的方式来获取对象。

三、单例模式的核心特点

  • 单例类只能有一个实例,即每次获取该类的对象都是同一个对象
  • 单例类必须自己创建自己的唯一实例,即 new 操作必须在单例类中实现,不能在外部 new 单例类
  • 单例类必须提供获取该唯一实例的方法,如Singleton.getInstance();

四、单例模式的多种实现方式

1、饿汉式

  • 简单来说就是不管用不用得上,在项目启动时就先 new 出该实例对象,这种方法比较简单,但可能会造成资源浪费,不过一般我们创建了这个类肯定就会使用,所以其实项目中直接用这种方式也是可以的
/*** 饿汉式单例模式 - 静态变量方式*/
public class Singleton {private final static Singleton INSTANCE = new Singleton();private Singleton() {}public static Singleton getInstance() {return INSTANCE;}}
  • 接下来请先思考以下几个问题,请务必认真思考后再看我的讲解

    • 问题一:为什么要私有化构造方法?
    • 问题二:为什么实例对象 INSTANCE 要加 static ?
    • 问题三:为什么实例对象 INSTANCE 要加 final ?
    • 问题四:除了以上这种写法,你能否给出与它功能一致的写法?
  • 答案如下:

    • 解答一:根据单例模式的特点,单例类必须自己创建自己的唯一实例,所以只能私有化构造方法,防止用户从外部 new 出新实例对象
    • 解答二:首先我们要知道我们的目的是获取该单例类的实例对象,由于不能 new 出该实例对象,所以我们只能通过类名直接调用类中的静态方法来获取该实例对象,即Singleton.getInstance() ,而静态方法是不能访问非静态成员变量的(因为非静态成员变量是随着对象的创建而被实例化的,而调用静态方法时,可能还没有实例化好对象,所以是无法访问非静态成员变量的),因此实例变量也必须是静态的,静态实例变量会在类的初次加载时初始化
    • 解答三:首先 final 保证了实例初始化过程的顺序性(这个我也不是很了解,感兴趣的可以去查阅 final 的相关资料),还有就是禁止通过反射方式改变实例对象,通过反射方式修改实例对象的方法请看我下面给出的SingletonTest 中的 testNotFinal() 方法
    • 解答四:上面我们是通过静态变量的方式实现饿汉式单例模式,还有一种方式的通过静态代码块的方式实现,具体看下面的 Singleton1,其实和上面这种方式没啥区别,主要就是把类实例化的过程放在了静态代码块中
public class SingletonTest {/*** 参考文章:https://www.jianshu.com/p/e22ca93024f3* Singleton 单例类不加 final 的时候可以通过反射方式修改实例对象,加上 final 则会抛出异常*/private static void testNotFinal() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {Singleton firstInstance = Singleton.getInstance();System.out.println("第一次拿到单例模式创建的对象:" + firstInstance);// 1.获得 Singleton 类Class<Singleton> clazz = Singleton.class;// 2.获得 Singleton 类的私有无参构造方法,并通过 setAccessible(true) 允许我们通过反射的方式调用该私有构造方法Constructor<Singleton> constructor = clazz.getDeclaredConstructor();constructor.setAccessible(true);// 3.创建新的实例对象Singleton clazzSingleton = constructor.newInstance();System.out.println("反射创建出来的对象: " + clazzSingleton);// 4.获取 Singleton 类中所有声明的字段,即包括public、private和 protected,但是不包括父类的字段,目前只有 INSTANCEField[] fields = clazz.getDeclaredFields();for (Field field : fields) {// 设置 true:允许通过反射访问该字段field.setAccessible(true);// 向 Singleton 对象(firstInstance)的这个 Field 属性(即:INSTANCE)设置新值 clazzSingletonfield.set(firstInstance, clazzSingleton);Singleton secondInstance = Singleton.getInstance();System.out.println("第二次拿到单例模式创建的对象: " + secondInstance);}}/*** 简单测试通过单例类(静态变量方式)获取的是否是同个对象*/public static void testSingleton() {Singleton singletonOne = Singleton.getInstance();Singleton singletonTwo = Singleton.getInstance();// 输出结果: trueSystem.out.println("两次获取的都是同一个对象:" + (singletonOne == singletonTwo));}public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {// 测试 Singleton 获取的是否是同个对象// testSingleton();// 测试不加 final 是否能成功修改 Singleton 类的实例对象testNotFinal();}
}
/*** 饿汉式单例模式 - 静态代码块方式*/
public class Singleton1 {// 注意:这里同样需要加 finalprivate final static Singleton1 INSTANCE;static {INSTANCE = new Singleton1();}private Singleton1() {}public static Singleton1 getInstance() {return INSTANCE;}}

2、懒汉式

  • 这里我们采用双重校验锁(DCL)的方式来实现懒汉式单例模式,这里看不懂没有关系,往下看,下面会给出一些问题以及懒汉式的其他错误写法,相信看完之后你就知道为什么要用DCL来实现懒汉式了
/*** 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式*/
public class Singleton {private static volatile Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {synchronized (Singleton.class) {if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
}

接下来我们先分析几种错误的懒汉式写法,通过以下这些分析和问题,相信读者就能明白上述代码

  • 第一种错误的懒汉式单例模式
public class SingletonErrorOne {private static SingletonErrorOne INSTANCE;private SingletonErrorOne() {}// 第一种错误的懒汉式单例模式,多线程下线程不安全,会产生多个实例public static SingletonErrorOne getInstance() {if (INSTANCE == null) {System.out.println(Thread.currentThread().getName() + ":开始生成新实例");INSTANCE = new SingletonErrorOne();}return INSTANCE;}
}

这种是我们最容易想到的懒汉式方式,即调用 getInstance() 方法的时候才创建实例对象,然后通过 if 判断让该实例对象只创建一次,但是只能在单线程下使用,在多线程下就会创建多个实例对象出来,这里举个例子说明一下:假设我们有线程一和线程二同时调用 getInstance() 方法,这时候两个线程的 INSTANCE 可能都是为 null 的,所以一定会生成新实例,我们可以通过下面代码测试一下

public class SingletonTest {public static void main(String[] args) {// 通过模拟多线程环境,我们可以看到有多个线程在生成新实例for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(SingletonErrorOne.getInstance().hashCode());}).start();}}
}
  • 第二种错误的懒汉式单例模式

第一种错误写法的问题是线程不安全,所以我们自然会想到通过加锁(synchronized )的方式来进行线程同步,防止产生多实例对象,代码如下:

public class SingletonErrorTwo {private static SingletonErrorTwo INSTANCE;private SingletonErrorTwo() {}// 这种方式虽然线程安全了但是性能太差了,已经退化为单线程,而且整个实例方法都被阻塞了// 如果该实例方法中存在耗时代码,将会大大降低接口性能,这时候我们可以降低锁粒度,只锁定部分代码public static synchronized SingletonErrorTwo getInstance() {try {// 耗时代码Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (INSTANCE == null) {System.out.println(Thread.currentThread().getName() + ":开始生成新实例");INSTANCE = new SingletonErrorTwo();}return INSTANCE;}
}

这种方式其实不算错误,只是我们不推荐使用,因为效率太低了,每次调用 getInstance 方法都会加锁,我们在实例方法里面加入一段耗时代码,这时候调用下面的测试方法,你将会发现效率特别的低下,这种就是锁粒度太大了,我们可以降低一下锁粒度,具体看第三种错误的懒汉式单例模式

public class SingletonTest {public static void main(String[] args) {// 通过模拟多线程环境,我们可以看到获取的都是同一个实例,但是执行效率特别的低下for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(SingletonErrorTwo.getInstance().hashCode());}).start();}}
}
  • 第三种错误的懒汉式单例模式

我们通过只锁定生成实例对象的这部分代码,让其他耗时代码并行执行,效率提高了,但是会产生多个实例对象,造成这种现象的原因是我们多个线程可能都通过了 if 判断,然后开始阻塞等待持有锁的线程释放锁,第一个持有锁的线程生成实例对象后释放锁,此时其他线程获得锁仍会继续生成新实例对象,因为已经通过了 if 判断,改进方法就是通过双重校验锁(DCL)来避免这种问题,我们获得锁之后可以再判断一次 INSTANCE 是否为空,这时候如果线程一是第一个获得锁的线程,那么线程一就会生成实例对象,如果线程二是第二个获得锁的线程,那么此时线程一已经生成完对象了,线程二就不会继续生成新对象,需要特别注意的是必须加上 volatile 字段,我们在下面进行讲解
注:这里由于我们加了生成新实例的打印输出,释放锁的速度比较慢,所以导致基本每个线程都会创建新实例,你可以去掉打印输出,你会发现虽然大部分输出的都是同个对象,但是还是会产生一些新对象,

public class SingletonErrorThree {private static SingletonErrorThree INSTANCE;private SingletonErrorThree() {}public static SingletonErrorThree getInstance() {try {// 耗时代码Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}if (INSTANCE == null) {synchronized (SingletonErrorThree.class) {System.out.println(Thread.currentThread().getName() + ":开始生成新实例");INSTANCE = new SingletonErrorThree();}}return INSTANCE;}
}
public class SingletonTest {public static void main(String[] args) {// 通过模拟多线程环境,我们可以看到执行效率大幅提升了,但是有多个线程在生成新实例for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(SingletonErrorThree.getInstance().hashCode());}).start();}}
}
  • 这里再给出双重校验锁实现懒汉式单例模式的代码,看完代码看下面给出的问题,看是否都能答得上来

    /*** 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式*/
    public class Singleton {private static volatile Singleton INSTANCE;private Singleton() {}public static Singleton getInstance() {if (INSTANCE == null) {synchronized (Singleton.class) {if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
    }
    
    • 问题一:为什么要私有化构造方法?
    • 问题二:为什么实例对象 INSTANCE 要加 static ?
    • 问题三:为什么要加 synchronized ?
    • 问题四:为什么要有第一个 if 判断 ?
    • 问题五:为什么要有第二个 if 判断 ?
    • 问题六:为什么实例对象 INSTANCE 要加 volatile ?
  • 答案如下:

    • 解答一:同饿汉式

    • 解答二:同饿汉式

    • 解答三:为了让线程同步,每次创建实例前先加锁,防止产生多实例

    • 解答四:为了提高效率,如果我们不加第一个 if 判断的话,那么每个线程都要加锁释放锁,但是其实我们只要第一个线程创建了实例对象后,后面的线程直接返回对象就好了,不需要再进行加锁操作

    • 解答五:没有第二个 if 判断会产生多实例的情况,具体看第三种错误的懒汉式单例模式的讲解

    • 解答六:我们知道 volatile 关键字有两大特性:可见性(变量修改后,所有线程都能立即实时地看到它的最新值)和禁止指令重排序;这里一开始我理解错了,我以为用到了可见性和指令重排序,可见性是因为第一个持有锁的线程创建完对象后,必须让其他线程知道,这样第二个 if 判断才会不为 null ,但是其实 synchronized 已经帮我们实现了线程的可见性,所以这里的 volatile 主要是起到禁止指令重排序的作用。

  • 下面我们来详细讲解为什么要禁止指令重排序
    我们要知道对象的创建其实不是一步到位的,它是分三步进行的,分别是
    ①、在堆中给对象分配内存空间
    ②、初始化赋值
    ③、建立关联(将引用指向分配的内存空间)
    现在我们的单例类中有个成员变量 a,代码如下:

/*** 懒汉式单例模式:双重校验锁(DCL,double-checked locking)方式*/
public class Singleton {private static volatile Singleton INSTANCE;int a = 1;private Singleton() {}public static Singleton getInstance() {...}
}

当我们第一个持有锁的线程执行到 INSTANCE = new Singleton(); 的时候正常是先分配内存空间,然后初始化给 a 赋值为 1,接着建立关联,让 INSTANCE 指向刚分配的内存空间,如果这个期间发生指令重排序,比如二三步骤调换顺序,这时候是先分配内存空间,然后就直接建立关联了,此时 a 还是默认值 0,其他线程会直接通过第二个 if 判断,因为此时已经建立关联了,所以 INSTANCE 已经不为空了,只是还没赋值为 1,这时候其他线程拿到的 a 可能为0,然后第一个持有锁的线程才将 a 赋值为1,所以我们必须禁止指令重排序,避免其他线程拿到半初始化状态的实例对象

3、静态内部类

这种方式的效果其实和饿汉式有点类似,都采用类加载机制确保创建的是单实例;但是饿汉式没有延迟初始化的功能,简单来说就是饿汉式不管你有没有调用,对象在类加载时就已经初始化了,而静态内部类方式只有调用getInstance时才会初始化静态内部类,进而初始化实例对象

public class Singleton {private Singleton() {}private static class SingletonInstance {private final static Singleton INSTANCE = new Singleton();}public static Singleton getInstance() {return SingletonInstance.INSTANCE;}
}

4、枚举

这种实现方式我没用过,这里就不做过多解释了,网上说这是实现单例模式的最佳方法,因为它更简洁,自动支持序列化机制,绝对防止多次实例化,不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

public enum Singleton {INSTANCE;public void add(int x, int y) {System.out.println(x + y);}// 其他各种方法// ...
}

测试方法如下:

public class SingletonTest {public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(() -> {System.out.println(Singleton.INSTANCE.hashCode());}).start();}}
}

五、如何解决序列化反序列化导致单例模式失效问题

  • 首先我们先实现 Serializable 接口,让单例类的对象可以序列化,代码如下:
public class Singleton implements Serializable {private final static Singleton INSTANCE = new Singleton();private Singleton() {}public static Singleton getInstance() {return INSTANCE;}}
  • 然后通过以下代码对实例对象序列化再反序列化,我们可以发现此时已经不是同个对象了
public class SingletonTest {public static void main(String[] args) throws IOException, ClassNotFoundException {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));oos.writeObject(Singleton.getInstance());File file = new File("tempFile");ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));Singleton singleton = (Singleton) ois.readObject();System.out.println(singleton);System.out.println(Singleton.getInstance());// 结果为:falseSystem.out.println(Singleton.getInstance() == singleton);}
}

解决方法如下:在单例对象代码中添加public Object readResolve()方法

public class Singleton implements Serializable {private final static Singleton INSTANCE = new Singleton();private Singleton() {}public static Singleton getInstance() {return INSTANCE;}public Object readResolve() {return getInstance();}
}

六、总结

希望各位读者看完能有所收获,有任何问题都可以在评论区讨论。