大数据知识体系
首页
数据结构与算法
  • JVM
  • Java
  • Scala
  • Python
设计模式
  • MySQL
  • Redis
  • HDFS
  • HBase
  • ClickHouse
  • ElasticSearch
  • Iceberg
  • Hudi
  • Spark
  • Flink
  • Hive
  • Yarn
  • Zookeeper
  • Maven
  • Git
  • 数据仓库
  • 用户画像
  • 指标体系
数据治理
关于
首页
数据结构与算法
  • JVM
  • Java
  • Scala
  • Python
设计模式
  • MySQL
  • Redis
  • HDFS
  • HBase
  • ClickHouse
  • ElasticSearch
  • Iceberg
  • Hudi
  • Spark
  • Flink
  • Hive
  • Yarn
  • Zookeeper
  • Maven
  • Git
  • 数据仓库
  • 用户画像
  • 指标体系
数据治理
关于
  • 设计模式概述
  • 创建型模式

    • 单例模式
      • 一、概述
        • 1.1 解决了什么问题
        • 1.2 解决方案
        • 1.3 实现步骤
      • 二、实现方式
        • 2.1 饿汉式(静态常量)
        • 2.2 饿汉式(静态代码块)
        • 2.3 懒汉式(不使用锁)
        • 2.4 懒汉式(同步方法)
        • 2.5 懒汉式(同步代码块)
        • 2.6 双重检查(推荐使用)
        • 2.7 静态内部类(推荐使用)
        • 2.8 枚举(推荐使用)
      • 三、源码中的应用
        • 3.1 java.lang.Runtime
    • 简单工厂模式
    • 工厂方法模式
    • 抽象工厂模式
    • 建造者模式
    • 原型模式
  • 结构型模式

    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型模式

    • 策略模式
    • 模板方法模式
    • 观察者模式
    • 迭代器模式
    • 责任链模式
    • 命令模式
    • 备忘录模式
    • 状态模式
    • 访问者模式
    • 中介者模式
    • 解释器模式
  • 设计模式
  • 创建型模式
Will
2022-03-29
目录

单例模式

# 一、概述

单例模式(Singleton)就是通过一定的方法保证某个类在整个系统中只有一个对象实例,且该类只有一个获取其对象的静态方法。如:一个学校只能有一个正校长,一个省份只能有一个省委书记。

# 1.1 解决了什么问题

单例模式同时解决了两个问题,违反了单一职责原则:

  1. 保证一个类只有一个实例。
  2. 为该实例提供一个全局访问节点。

# 1.2 解决方案

  1. 保证一个类只有一个实例。

    构造函数私有化。

  2. 为该实例提供一个全局访问节点。

    新建一个静态构建方法作为构造函数。

# 1.3 实现步骤

  1. 在类中添加一个私有静态成员变量用于保存单例实例。
  2. 声明一个公有静态构建方法用于获取单例实例。
  3. 在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
  4. 将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其它对象不能调用。
  5. 检查客户端代码,将对单例的构造函数的调用替换为对其静态构建方法的调用。

# 二、实现方式

单例模式有 8 种实现方式:

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(不使用锁)
  • 懒汉式(同步方法)
  • 懒汉式(同步代码块)
  • 双重检查(推荐使用)
  • 静态内部类(推荐使用)
  • 枚举(推荐使用)

饿汉式的意思就是像饿死鬼一样,不管用不用,上来就先实例化,也不在乎什么浪费不浪费。饿汉式天生就是线程安全的。

懒汉式就是没有预先实例化的这种意识,只有用到了才去实例化。懒汉式天生就是线程不安全的,需要通过各种手段让其线程安全。

# 2.1 饿汉式(静态常量)

/**
 * 饿汉式(静态常量)
 */
class Singleton{

    private Singleton(){}

    private final static Singleton instance = new Singleton();

    public static Singleton getInstance(){
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

特点:

  1. 在类加载的时候完成实例化,可以避免线程同步问题。
  2. 但是没有达到懒加载的效果,如果整个系统从始至终没有用到过整个实例,则有点浪费。

# 2.2 饿汉式(静态代码块)

/**
 * 饿汉式(静态代码块)
 */
class Singleton{

    private Singleton(){}

    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    public static Singleton getInstance(){
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

特点:

和饿汉式(静态常量)的实现方式类似,只是将实例化的过程放在了静态代码块中,可以解决线程同步问题,但是没有达到懒加载的效果,可能会造成内存浪费。

# 2.3 懒汉式(不使用锁)

/**
 * 懒汉式(不使用锁)
 */
class Singleton{

    private Singleton(){}

    private static Singleton instance;

    public static Singleton getInstance(){

        if (instance == null){
            instance = new Singleton();
        }

        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

特点:

  1. 这种方式实现了懒加载,只有在用到的时候才去实例化。
  2. 这种方式只能在单线程环境使用,多线程环境下不安全,有可能多个线程同时在执行new Singleton()操作。

# 2.4 懒汉式(同步方法)

/**
 * 懒汉式(同步方法)
 */
class Singleton {

    private Singleton() {}

    private static Singleton instance;

    public static synchronized Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

特点:

  1. 解决了线程安全问题,也解决了懒加载的问题。
  2. 执行效率太低,实际上只有实例化的那部分代码需要线程同步,一旦实例化之后锁就没意义了。如果整个方法都同步的话,那么多个线程都想执行getInstance()的时候,同一时刻只有一个线程可以获得。

# 2.5 懒汉式(同步代码块)

/**
 * 懒汉式(同步代码块)
 */
class Singleton {

    private Singleton(){}

    private static Singleton instance;

    public static Singleton getInstance() {

        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

特点:

这种方式加了全局唯一锁,看起来是线程安全的,实际上是个陷阱,说到底还是不安全的。在单例对象还未初始化的时候,线程 A 和 B 都是可以执行到if (instance == null) {}内部的,此时线程 A 拿到了锁,执行instance = new Singleton();部分,线程 B 在等待。等到线程 A 执行结束之后释放锁,线程 B 也会执行instance = new Singleton();操作,这样实际上执行了两次实例化操作,获取的是两个不同的对象,所以是不安全的。

# 2.6 双重检查(推荐使用)

推荐

《阿里巴巴 Java 开发手册》约定使用这种方式。

/**
 * 双重检查
 */
class Singleton {

    private Singleton() {
    }

    private static volatile Singleton instance;

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

        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

为什么需要两个 `if (instance == null)`判断?

如果像下面这样只有一个判断,实际上就是懒汉式(同步代码块)这种方式。在 instance 还未实例化的时候,如果有两个线程执行 if 语句,则两个语句都会进入 if 语句内部,且都会执行实例化操作,只是一前一后执行。synchronized关键字只是保证在同一时刻只有一个在线程在进行操作。

if (instance == null){
    synchronized (Singleton.class) {
        instance = new Singleton();
    }
}
1
2
3
4
5

为什么需要 volatile 关键字?

volatile 关键字的作用是禁止指令重排。在使用javap -c -v -p Singleton.class 命令进行反编译,会发现里面有三个主要步骤:

  1. new:在内存中分配空间用于保存对象(只分配空间,未初始化)。
  2. invokespecial:调用构造方法,完成单例对象初始化。
  3. putstatic:将内存地址赋值给成员变量。

上面三个步骤执行完才算是完成了单例对象的初始化,正常顺序应该是从上往下依次执行,但是 Java 虚拟机在执行步骤没有必要关联的情况下是可以重新排序的,重新排序在单线程情况下是没有问题的,因为最终时候的时候都会执行该内存地址。但是在多线程环境下可能会出现问题。

假设在多线程环境下发生了指令重排,其执行顺序为 new、putstatic、invokespecial。线程 A 执行了前两个,该对象已经不是 null,而且 instance 成员变量已经有了内存地址(但是没有调用构造方法)。此时刚好线程 CPU 切换至线程 B 执行,由于线程执行了 putstatic 命令,所以线程 B 认为其已经完成了实例化操作(实际上还未调用构造方法),所以使用是有问题的。

volatile 关键字的作用就是禁止指令重排,让它老老实实按照前后顺序执行。

特点:

  1. 双重检查模式就是对懒汉式(同步代码块)方式的优化。
  2. 实现了线程安全、懒加载,且访问效率高。

# 2.7 静态内部类(推荐使用)

/**
 * 静态内部类
 */
class Singleton {

    private Singleton() {
    }

    private static volatile Singleton instance;

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static synchronized Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

特点:

利用类加载机制实现了懒加载,同时也实现了线程安全,效率高。

# 2.8 枚举(推荐使用)

public class SingletonTest {
    public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
    }
}

/**
 * 枚举
 */
enum Singleton {
    INSTANCE;
}
1
2
3
4
5
6
7
8
9
10
11
12

特点:

利用枚举实现单例,这种思路真的很奇特。使用简单、可以避免线程同步问题、效率又高。

# 三、源码中的应用

# 3.1 java.lang.Runtime

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上次更新: 2023/11/01, 03:11:44

← 设计模式概述 简单工厂模式→

Theme by Vdoing | Copyright © 2022-2023 Will 蜀ICP备2022002285号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式