深入理解单例设计模式
作者:惜鸟
来源:Segmentfault 思否
一、概述
单例模式是什么 单例模式的使用场景 单例模式的优缺点 单例模式的实现(重点) 总结
二、单例模式是什么
三、单例模式的使用场景
需要生成唯一序列的环境 需要频繁实例化然后销毁的对象。 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 方便资源相互通信的环境
四、单例模式的优缺点
在内存中只有一个对象,节省内存空间; 避免频繁的创建销毁对象,减轻 GC 工作,同时可以提高性能; 避免对共享资源的多重占用,简化访问; 为整个系统提供一个全局访问点。
不适用于变化频繁的对象; 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出; 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
五、单例模式的实现(重点)
私有化构造方法,避免外部类通过 new 创建对象 定义一个私有的静态变量持有自己的类型 对外提供一个静态的公共方法来获取实例 如果实现了序列化接口需要保证反序列化不会重新创建对象
1、饿汉式,线程安全
缺点:不是懒加载,类加载时就初始化,浪费内存空间
/**
* 饿汉式单例测试
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造方法
private Singleton(){}
// 2、定义一个静态变量指向自己类型
private final static Singleton instance = new Singleton();
// 3、对外提供一个公共的方法获取实例
public static Singleton getInstance() {
return instance;
}
}
public class Test {
public static void main(String[] args) throws Exception{
// 使用反射破坏单例
// 获取空参构造方法
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
// 设置强制访问
declaredConstructor.setAccessible(true);
// 创建实例
Singleton singleton = declaredConstructor.newInstance();
System.out.println("反射创建的实例" + singleton);
System.out.println("正常创建的实例" + Singleton.getInstance());
System.out.println("正常创建的实例" + Singleton.getInstance());
}
}
反射创建的实例com.example.spring.demo.single.Singleton@6267c3bb
正常创建的实例com.example.spring.demo.single.Singleton@533ddba
正常创建的实例com.example.spring.demo.single.Singleton@533ddba
2、懒汉式,线程不安全
缺点:线程不安全
/**
* 懒汉式单例,线程不安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造方法
private Singleton(){ }
// 2、定义一个静态变量指向自己类型
private static Singleton instance;
// 3、对外提供一个公共的方法获取实例
public static Singleton getInstance() {
// 判断为 null 的时候再创建对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("多线程创建的单例:" + Singleton.getInstance());
}).start();
}
}
}
多线程创建的单例:com.example.spring.demo.single.Singleton@18396bd5
多线程创建的单例:com.example.spring.demo.single.Singleton@7f23db98
多线程创建的单例:com.example.spring.demo.single.Singleton@5000d44
3、懒汉式,线程安全
synchronized
关键字加锁保证线程安全,synchronized
可以添加在方法上面,也可以添加在代码块上面,这里演示添加在方法上面,存在的问题是每一次调用 getInstance
获取实例时都需要加锁和释放锁,这样是非常影响性能的。缺点:效率较低
/**
* 懒汉式单例,方法上面添加 synchronized 保证线程安全
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造方法
private Singleton(){ }
// 2、定义一个静态变量指向自己类型
private static Singleton instance;
// 3、对外提供一个公共的方法获取实例
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
4、双重检查锁(DCL, 即 double-checked locking)
/**
* 双重检查锁(DCL, 即 double-checked locking)
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造方法
private Singleton() {
}
// 2、定义一个静态变量指向自己类型
private volatile static Singleton instance;
// 3、对外提供一个公共的方法获取实例
public synchronized static Singleton getInstance() {
// 第一重检查是否为 null
if (instance == null) {
// 使用 synchronized 加锁
synchronized (Singleton.class) {
// 第二重检查是否为 null
if (instance == null) {
// new 关键字创建对象不是原子操作
instance = new Singleton();
}
}
}
return instance;
}
}
缺点:实现较复杂
volatile
关键字的使用,关于 volatile
的详细介绍可以直接搜索 volatile 关键字即可,有很多写的非常好的文章,这里不做详细介绍,简单说明一下,双重检查锁中使用 volatile
的两个重要特性:可见性、禁止指令重排序volatile
?new
关键字创建对象不是原子操作,创建一个对象会经历下面的步骤:在堆内存开辟内存空间 调用构造方法,初始化对象 引用变量指向堆内存空间
1 2 3
或者 1 3 2
,因此当某个线程在乱序运行 1 3 2
指令的时候,引用变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进入了 getInstance
的第一个 if(instance == null)
判断不为 nulll ,导致错误使用了没有初始化的非 null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。volatile
关键字以后,会通过在创建对象指令的前后添加内存屏障来禁止指令重排序,就可以避免这个问题,而且对 volatile
修饰的变量的修改对其他任何线程都是可见的。5、静态内部类
/**
* 静态内部类实现单例
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public class Singleton {
// 1、私有化构造方法
private Singleton() {
}
// 2、对外提供获取实例的公共方法
public static Singleton getInstance() {
return InnerClass.INSTANCE;
}
// 定义静态内部类
private static class InnerClass{
private final static Singleton INSTANCE = new Singleton();
}
}
遇到 new
、getstatic
、putstatic
、invokestatic
这4条字节码指令时。生成这4条指令最常见的 Java 代码场景是:使用new
关键字实例化对象的时候、读取或设置一个类的静态字段(final修饰除外,被final修饰的静态字段是常量,已在编译期把结果放入常量池)的时候,以及调用一个类的静态方法的时候。使用 java.lang.reflect
包方法对类进行反射调用的时候。当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()的那个类),虚拟机会先初始化这个主类。 当使用JDK 1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果是REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,则需要先触发这个方法句柄所对应的类的初始化。
INSTANCE
在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>()
方法完毕。如果在一个类的 <clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()
方法后,其他线程唤醒之后不会再次进入<clinit>()
方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。6、枚举单例
/**
* 枚举实现单例
*
* @className: Singleton
* @date: 2021/6/7 14:32
*/
public enum Singleton {
INSTANCE;
public void doSomething(String str) {
System.out.println(str);
}
}
Singleton singleton = Singleton.INSTANCE;
javap Singleton.class
Compiled from "Singleton.java"
public final class com.spring.demo.singleton.Singleton extends java.lang.Enum<com.spring.demo.singleton.Singleton> {
public static final com.spring.demo.singleton.Singleton INSTANCE;
public static com.spring.demo.singleton.Singleton[] values();
public static com.spring.demo.singleton.Singleton valueOf(java.lang.String);
public void doSomething(java.lang.String);
static {};
}
static final
修饰,所以可以通过类名直接调用,并且创建对象的实例是在静态代码块中创建的,因为 static 类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,所以创建一个enum类型是线程安全的。public class Test {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.INSTANCE;
singleton.doSomething("hello enum");
// 尝试使用反射破坏单例
// 枚举类没有空参构造方法,反编译后可以看到枚举有一个两个参数的构造方法
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class, int.class);
// 设置强制访问
declaredConstructor.setAccessible(true);
// 创建实例,这里会报错,因为无法通过反射创建枚举的实例
Singleton enumSingleton = declaredConstructor.newInstance();
System.out.println(enumSingleton);
}
}
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:492)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at com.spring.demo.singleton.Test.main(Test.java:24)
newInstance()
方法,有如下判断:六、总结
public class Singleton implements Serializable {
// 1、私有化构造方法
private Singleton() {
}
// 2、对外提供获取实例的公共方法
public static Singleton getInstance() {
return InnerClass.instance;
}
// 定义静态内部类
private static class InnerClass{
private final static Singleton instance = new Singleton();
}
// 对象被反序列化之后,这个方法立即被调用,我们重写这个方法返回单例对象.
protected Object readResolve() {
return getInstance();
}
}
多线程- 在多线程应用程序中必须使用单例时,应特别小心。
序列化- 当单例实现 Serializable 接口时,他们必须实现 readResolve 方法以避免有 2 个不同的对象。
类加载器- 如果 Singleton 类由 2 个不同的类加载器加载,我们将有 2 个不同的类,每个类加载一个。
由类名表示的全局访问点- 使用类名获取单例实例。这是一种访问它的简单方法,但它不是很灵活。如果我们需要替换Sigleton类,代码中的所有引用都应该相应地改变。
评论