这篇文章本来还是按照上一篇的风格写的,但发现太耗费精力的,于是变成了最简单粗暴的版本。后面需要学会在有趣与直接、丰富与简洁之间找到平衡。
单例模式(Singleton Pattern)是一种设计模式,用于确保一个类只有一个实例,并提供对该实例的全局访问点。 单例模式的使用场景大多和系统资源相关,比如缓存、线程池和数据库连接池等,在这些场景中,如果实例化多个对象,可能会出现程序执行结果不一致或者系统资源耗费严重的问题。
Intent
The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.
Class Diagram
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
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 代码块,从而实例化多个对象,因此这种方法没啥用。为了解决该问题,我们可以想到几个解决方案:
- 定义时就new,这样类加载时就会实例化对象。
- 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;
}
}
看到上面的代码你可能会有两个疑问。
- 为什么成员变量前加了个volatile?这是因为JVM具有指令重排的特性,
singleton = new Singleton();
这条语句可能会按照这样的顺序执行:①分配地址空间,②引用指向这块地址,③对象进行初始化。这样的话,可能对象还没有完成初始化就被某个线程拿到了,而volatile可以禁止指令重排。 - 为什么判断两次singleton是否为null?因为第一个if没加锁,可能有多个线程同时进入。如果没有第二个if判断,尽管使用了synchronized,已经进入的线程会依次实例化一个对象。
双重校验锁方法有什么问题吗?还是有的,它无法适用反射、序列化的场景。下面将介绍单例模式的最佳实践:枚举。
枚举实现
package org.kuilz.SingletonPattern.EnumSafe;
public enum Singleton {
UNIQUE_INSTANCE;
}
枚举是单例模式的最佳实践。 看到这你是不是觉得前面的都白学了?其实并没有,经过前面拍拍脑袋想出来线程不安全的懒汉式,到一步步改进实现其他方法,这个过程我们对单例模式的理解更加深刻了。
ps:本文源码放在github上,配合博客食用更佳。