单例模式——8种实现方式

930 阅读8分钟
前言

在一些场景中,我们希望创建的对象在整个软件系统只保存一份实例,如线程池, 日志对象、缓存等。创建并保存对象单一主要有两个作用:节省系统资源;防止多个对象产生冲突。**单例模式(Singleton Pattern)**就可以确保只有一个实例对象会被创建。今天,我们重点聊聊单例模式的8种实现方式(java语言)

private修饰符

我们都知道,可以通过 new 的方式创建对象。如果类对new方式创建对象不加以约束的话,就不能保证系统只创建一个对象。private修饰类的构造方法,就可以确保该类不能任意创建对象。

一、饿汉式(静态常量)

饿汉式实现单例模式的原理:利用静态常量在类加载时生成全局唯一实例特性

具体代码

// 单例模式实现1,饿汉式(静态常量)
public class Singleton1 {
    // 类加载时,实例化对象
    private static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance() {
        return instance;
    }

    private Singleton1() {
        System.out.println("单例模式实现1,饿汉式(静态常量)");
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态常量方式创建单例对象:");
        Singleton1 instance1 = Singleton1.getInstance();
        Singleton1 instance2 = Singleton1.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行main方法,结果如下:
单例模式实现1,饿汉式(静态变量)
开始演示静态变量方式创建单例对象:      
true

从运行结果(输出打印的1,2行顺序)可以看出,我们想要获取的实例对象在真正获取之前已经实例化。(静态常量在类加载过程中赋值)

这也是饿汉式实现单例模式不好的一点:不能懒加载。

二、饿汉式(静态代码块)

基本与上面的实现方式一样,只是语法有点区别,静态代码块替换静态变量直接赋值。

// 单例模式实现2,饿汉式(静态代码块)
public class Singleton2 {

    static {
        instance = new Singleton2();
    }

    private static Singleton2 instance;

    private Singleton2() {
        System.out.println("单例模式实现2,饿汉式(静态代码块)");
    }

    public static Singleton2 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态代码块方式创建单例对象:");
        Singleton2 instance1 = Singleton2.getInstance();
        Singleton2 instance2 = Singleton2.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行main方法,结果如下:
单例模式实现2,饿汉式(静态代码块)
开始演示静态代码块方式创建单例对象:
true
三、懒汉式(常规写法,线程不安全)

上面的两种写法,都是不支持懒加载的。接下来的几种方式,都是懒加载的方式。首先看看最简单的一种实现方式

// 单例模式实现3,懒汉式(常规写法,线程不安全)
public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
        System.out.println("单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:" + Thread.currentThread().getName());
    }

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

    public static void main(String[] args) {
        System.out.println("开始演示常规懒加载方式创建单例对象:");
        Singleton3 instance1 = Singleton3.getInstance();
        Singleton3 instance2 = Singleton3.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行结果
开始演示常规懒加载方式创建单例对象:
单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:main
true

从运行结果来看,这种方式似乎没有问题。即实现了懒加载,又保证了对象单一。

我们换种演示方式,修改main方法:

    public static void main(String[] args) {
        System.out.println("开始演示常规懒加载方式创建单例对象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton3.getInstance();
            }).start();
        }
    }
    // 运行结果(有可能需要多运行几次,才会出现类似效果)
    开始演示常规懒加载方式创建单例对象:
	单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:Thread-1
	单例模式实现3,懒汉式(常规写法,线程不安全)。当前线程:Thread-0

从运行结果可以看出,这种单例模式的实现方式是线程不安全的,在多线程环境下,有可能会创建多个实例。

四、懒汉式(同步方法,线程安全)

方式三创建单例对象,线程不安全的原因是:当instance在完成实例化之前,多个线程同时判断if (instance == null)结果都为true,导致这些线程都往下继续执行创建实例对象。简单粗暴的解决方式,在getInstance方法加锁(用synchronized关键字修饰方法)。具体代码:

// 单例模式实现4,懒汉式(同步方法,线程安全)
public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {
        System.out.println("单例模式实现4,懒汉式(同步方法,线程安全)。当前线程:" + Thread.currentThread().getName());
    }

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

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-同步方法方式创建单例对象:");
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton4.getInstance();
            }).start();
        }
    }
}
// 运行结果
开始演示懒加载-同步方法方式创建单例对象:
单例模式实现4,懒汉式(同步方法,线程安全)。当前线程:Thread-1

这种方式,虽然解决了线程安全问题,但是每次获取实例对象时,都需要加锁,这大大影响了系统运行效率。接下来的实现方式,将逐步优化线程安全下懒加载效率低的问题。

五、懒汉式(同步代码块)

在静态方法加锁,锁粒度太大,造成资源浪费。因此,我们尝试把锁粒度缩小,在代码块加锁。

示例代码:

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
        System.out.println("单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:" + Thread.currentThread().getName());
    }

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

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-同步代码块方式创建单例对象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton5.getInstance();
            }).start();
        }
    }
}
// 运行结果(有可能需要多运行几次,才会出现类似效果):
开始演示懒加载-同步代码块方式创建单例对象:
单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:Thread-0
单例模式实现5,懒汉式(同步代码块,线程安全)。当前线程:Thread-1

从运行结果来看,这种实现方式也是线程不安全的。原因分析:

关键代码

        if (instance == null) {                     // 第1行
            synchronized (Singleton5.class){        // 第2行
                instance = new Singleton5();        // 第3行
            }
        }

虽然在2行加上了锁,但这只保证了同一时刻,只有一个线程可以执行第3行代码。在第3行代码执行前,不同的线程还是可以判断if是true,然后执行到第2行,等待有锁的线程释放锁,获得锁之后继续创建对象。

若要线程安全,改造如下:

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

然而,这种实现方式与第四种效果一致,锁的粒度是整个getInstance方法。

六、懒汉式(双重检查)

前面三种懒加载实现单例的方式,都有各自的不足,不是线程不安全就是获取单例效率低。线程不安全的地方在于已有线程创建实例,继续创建实例。效率低的地方在于,已经创建好实例,还加锁获取实例。而双重检查就避免了这两种问题。

示例代码

// 懒汉式(双重检查)
public class Singleton6 {

    private static volatile Singleton6 instance;

    private Singleton6() {
        System.out.println("单例模式实现6,懒汉式(双重检查)。当前线程:" + Thread.currentThread().getName());
    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            System.out.println("尝试创建实例...");
            synchronized (Singleton6.class){
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示懒加载-双重检查方式创建单例对象:");

        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                Singleton6.getInstance();
            }).start();
        }
    }
}
// 运行结果
开始演示懒加载-双重检查方式创建单例对象:
尝试创建实例...
单例模式实现6,懒汉式(双重检查)。当前线程:Thread-0

特别注意一点,我们静态变量用了「volatile」关键词修饰,为什么要用volatile修饰呢,可以参考文章:《双重检查锁定与延迟初始化》

七、静态内部类

利用静态内部类的方式,我们也可以实现线程安全的单例模式

示例代码

// 单例模式实现7,静态内部类
public class Singleton7 {

    private Singleton7(){
        System.out.println("单例模式实现7静态内部类。当前线程:" + Thread.currentThread().getName());
    }

    private static class InstanceHolder{
        private static Singleton7 instance = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return InstanceHolder.instance;
    }

    public static void main(String[] args) {
        System.out.println("开始演示静态内部类创建单例对象:");
        Singleton7 instance1 = Singleton7.getInstance();
        Singleton7 instance2 = Singleton7.getInstance();
        System.out.println(instance1 == instance2);
    }
}
// 运行结果
开始演示静态内部类创建单例对象:
单例模式实现7静态内部类。当前线程:main
true

JVM 帮助我们保证了内部类创建的线程安全性

八、枚举方式

枚举在jvm里是天然的单例,所以利用枚举实现单例也是线程安全的。《 Effective Java》这本书就提倡用枚举的方式创建单例对象

示例代码

public enum Singleton8 {

    INSTANCE();

    Singleton8(){
        System.out.println("单例模式实现8,枚举方式");
    }
}
总结

单例模式的实现方式有多种,保证线程安全和运行效率情况下(文中的第三种方式线程不安全,第四、五种方式效率低)),各种实现方式的实际效果差别并不大,选择自己顺手的实现方式就可以!而「懒加载」和「双重检查」思想,在我们开发中经常使用到的,希望大家好好理解这两种思想。