单例模式

介绍

优点:节约内存和保证共享计算的结果正确,以及方便管理。

应用:

  • 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
  • 无状态工具类:类似于整个系统的日志对象,我们只需要一个单例日志对象负责记录,管理系统日志信息。

写法

单例模式有 8 种,很多时候存在 饿汉式单例 以及 懒汉式单例 的概念。

饿汉式单例: 在获取单例对象之前对象已经创建完成了。
懒汉式单例: 在真正需要单例对象的时候才创建出该对象。

写法很多,给出一种推荐写法,推荐面试使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 双重检查,延迟加载
*/
public class Singleton01 {
// volatile 保证指令不会重排序和可见性
private volatile static Singleton01 INSTANCE = null;

private Singleton01() {

}

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

为什么静态成员变量要加上 volatile 关键字呢?

解释该问题之前,我们需要知道 Java 创建对象的步骤:
a.分配内存空间
b.调用构造器,初始化实例
c.返回地址给引用

在这几步中,底层可能进行对象创建的重排序操作,b、c 步骤在执行的过程中会互换:
a.分配内存空间
b.返回地址给引用(此时静态变量已经不为null了,对象还没有初始化)
c.调用构造器,初始化实例

如果不使用 volatile 关键字,可能会发生指令重排序。下面使用 A、B 线程举例。

线程A:
1.分配内存空间
2.返回地址给引用
3.调用构造器,初始化实例

线程B:
认为静态变量不为null,直接得到对象的内存地址。但是该对象还没有初始化完成,如果线程B使用该对象去操作,会出现部分值缺失(NPE)。

所以为了确保线程安全,我们需要加上该关键字。

静态内部类单例模式

基于类的初始化实现延迟加载和线程安全的单例设计。

JVM 在类的初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会获得一个锁,这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton02 {

private Singleton02() {

}

// 静态内部类
private static class InnerClass {
// JVM 在类的初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。
// 在执行类的初始化期间,JVM 会获得一个锁,这个锁可以同步多个线程对同一个类的初始化。
// 基于这个特性,可以实现另一种线程安全的延迟初始化方案。
private static final Singleton02 INSTANCE = new Singleton02();
}

public static Singleton02 getInstance() {
return InnerClass.INSTANCE;
}
}

静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。
通过对比基于 volatile 的双重检查锁定方案和基于类的初始化方案的对比,我们会发现基于类的初始化的方案的实现代码更为简洁。但是基于 volatile 的双重检查方案有一个额外的优势: 除了可以对静态字段实现延迟加载初始化,还可以对实例字段实现延迟初始化。

枚举实现单例

枚举实际上是一种多例的模式,如果我们只定义一个实例就是单例了。

1
2
3
4
5
6
public enum Singleon03 {
INSTANCE;
public void whatever() {
...
}
}

枚举是 Java 提供的一种特性,已经实现了线程安全机制。

枚举实现单例只是一种实现策略,或者面试的回答方案,实际开发中通常不使用该方式。通常被用来作为信息的标志和分类。

策略模式

策略模式也是非常常用的一种模式,使用策略模式可以轻松扩展第三方服务,非常适合 if 比较多的情况。为什么这么说呢?主要是它和模板模式比较像,这样说区别比较明显。

顾名思义,策略模式使用时至少有两种不同的策略(简单理解为两个 if),所以当根据多个不同的条件走不同的分支时非常灵活,而且方便扩展。策略模式是符合开闭原则的。

模式举例

以快递举例吧,比如我国有很多的快递公司:京东、韵达、圆通、中通、申通等等。当你的系统集成这些服务时,在用户下单的时候,需要根据条件去选择不同的快递,每家快递的收费情况、运输质量等也是不同的。如果使用 if-else-if 会有大段大段的代码块,后期根据快递公司的业务变动维护自己的系统要改一些地方,这时候就比较头疼了。

这个时候就可以考虑使用策略模式,定义通用的方法在一个接口中,然后为每家快递公司创建一个类去实现这个接口,为每家快递公司编写自己的实现就可以了。

说到这里,你可能觉得它和模板模式很类似。所以接下来说说区别。

模板方法通常是抽象模板,里面通常包含两部分:通用的算法实现;留给子类实现的抽象方法。

策略模式一般使用接口提供,由具体的实现类编写处理逻辑,提供算法。

可以看到,模板方法模式一般只针对一套算法,对同一套算法的不同实现细节进行抽象;而策略模式对多种算法进行不同的实现,所以策略模式相比模板模式更加灵活,算法与算法之间一般没有冗余代码。
策略模式的关注点更广,模板模式的关注点更深。它们不能相互替换,但可以组合使用。


本站由 江湖浪子 使用 Stellar 1.29.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。