设计模式——单例模式

kuilz 2024-05-13 {System Design} [Design Pattern]

这篇文章本来还是按照上一篇的风格写的,但发现太耗费精力的,于是变成了最简单粗暴的版本。后面需要学会在有趣与直接、丰富与简洁之间找到平衡。

单例模式(Singleton Pattern)是一种设计模式,用于确保一个类只有一个实例,并提供对该实例的全局访问点。 单例模式的使用场景大多和系统资源相关,比如缓存、线程池和数据库连接池等,在这些场景中,如果实例化多个对象,可能会出现程序执行结果不一致或者系统资源耗费严重的问题。

Intent

The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.

Class Diagram

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

img

Implementation

懒汉式-线程不安全

Singleton class:

package org.kuilz.SingletonPattern.LazyUnsafe;

public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

SingletonTest class:

package org.kuilz.SingletonPattern.LazyUnsafe;

public class SingletonTest {
    public static void main(String[] args) {
        int numThreads = 10;
        Thread[] threads = new Thread[numThreads];
        for (int i = 0; i < numThreads; i++) {
            threads[i] = new Thread(new MyRunnable());
            threads[i].start();
        }
    }
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            Singleton singleton = Singleton.getInstance();
            System.out.println("Thread: " + Thread.currentThread().getId() + ", Singleton hashCode: " + singleton.hashCode());
        }
    }
}

Output:

Thread: 26, Singleton hashCode: 43190021
Thread: 27, Singleton hashCode: 43190021
Thread: 22, Singleton hashCode: 43190021
Thread: 28, Singleton hashCode: 43190021
Thread: 23, Singleton hashCode: 43190021
Thread: 29, Singleton hashCode: 43190021
Thread: 24, Singleton hashCode: 43190021
Thread: 21, Singleton hashCode: 43190021
Thread: 20, Singleton hashCode: 1102734481
Thread: 25, Singleton hashCode: 2046608170

这是我们最容易想出来的实现,但多线程并发时,可能有多个线程进入 if 代码块,从而实例化多个对象,因此这种方法没啥用。为了解决该问题,我们可以想到几个解决方案:

  1. 定义时就new,这样类加载时就会实例化对象。
  2. getter方法上添加synchronized关键字,保证每次只有一个线程进入。

这两种方法都是线程安全的,分别对应饿汉式-线程安全和懒汉式-线程安全。这名字起了不如不起🥲,反而让人记不住。我们只需要记住懒汉式指的是lazy加载,需要用到时才实例化。饿汉式指的是eager加载,类加载器加载类时就进行实例化。

饿汉式-线程安全

package org.kuilz.SingletonPattern.EargeSafe;

public class Singleton {
    private static Singleton singleton=new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return singleton;
    }
}

这种方法的缺点是就算你用不到该对象,也会实例化出来,所以适用于资源充裕的情况。

懒汉式-线程安全

package org.kuilz.SingletonPattern.LazySafe;

public class Singleton {
    private static Singleton singleton;
    private Singleton(){}
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

synchronized可能会使性能下降100倍,而且我们其实只有第一次访问getter时需要加锁,所以这种写法极大的降低了性能,适用于对性能要求不高的情况。

怎么解决呢?可以自然地想到,那就不对整个函数使用synchronized加锁了,只对new 对象的代码块用就好了,于是引出了双重校验锁方法。

双重校验锁-线程安全

package org.kuilz.SingletonPattern.DoubleCheckSafe;

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton(){}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class){
                if(singleton==null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

看到上面的代码你可能会有两个疑问。

  1. 为什么成员变量前加了个volatile?这是因为JVM具有指令重排的特性,singleton = new Singleton();这条语句可能会按照这样的顺序执行:①分配地址空间,②引用指向这块地址,③对象进行初始化。这样的话,可能对象还没有完成初始化就被某个线程拿到了,而volatile可以禁止指令重排。
  2. 为什么判断两次singleton是否为null?因为第一个if没加锁,可能有多个线程同时进入。如果没有第二个if判断,尽管使用了synchronized,已经进入的线程会依次实例化一个对象。

双重校验锁方法有什么问题吗?还是有的,它无法适用反射、序列化的场景。下面将介绍单例模式的最佳实践:枚举。

枚举实现

package org.kuilz.SingletonPattern.EnumSafe;

public enum Singleton {
    UNIQUE_INSTANCE;
}

枚举是单例模式的最佳实践。 看到这你是不是觉得前面的都白学了?其实并没有,经过前面拍拍脑袋想出来线程不安全的懒汉式,到一步步改进实现其他方法,这个过程我们对单例模式的理解更加深刻了。

ps:本文源码放在github上,配合博客食用更佳。

JDK

References

  1. Code Repo
  2. 《Head First Design Pattern》
  3. Java 全栈知识体系
💬评论