设计模式——单例模式
创始人
2025-05-28 18:14:03
0

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创
建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供
了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

一、饿汉单例模式

单例设计模式分类两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

1.静态变量实现饿汉式单例模式

package 单例模式.饿汉式_静态成员变量;
/*** @author Watching* * @date 2023/3/14* * Describe:饿汉模式:静态成员变量方式*/
public class HungerSingleton {//1.私有构造方法private HungerSingleton() {}//2.静态私有成员private static HungerSingleton instance = new HungerSingleton();//3.提供一个静态的方法获取唯一实例public static HungerSingleton getInstance() {return instance;}
}

2.静态代码块实现饿汉式单例模式

package 单例模式.饿汉式_静态代码块;
/*** @author Watching* * @date 2023/3/14* * Describe:饿汉式:静态代码块*/
public class HungerSingleton {//1.私有构造方法private HungerSingleton() {}//2.私有静态成员private static HungerSingleton instance;//3.静态代码块static {instance = new HungerSingleton();}//4.提供公有静态方法访问唯一对象public static HungerSingleton getInstance() {return instance;}
}

3.最简单的饿汉式单例模式——枚举类

1.创建一个枚举类

public enum Singleton {INSTANCE;
}

2.获取这个对象

public class Client {public static void main(String[] args) {Singleton instance1 = Singleton.INSTANCE;Singleton instance2 = Singleton.INSTANCE;System.out.println(instance1 == instance2);}
}

两次获得对象是同一个对象

总结

饿汉式单例模式的三个要点
①私有构造方法
②私有静态成员
③直接初始化静态成员,或者在静态代码块中初始化
④提供静态方法获取单例

二、懒汉单例模式

1.静态内部类实现懒汉单例模式

1.使用静态内部类创建单例类

public class LazySingleton {//1.创建一个私有构造器private LazySingleton() {}//2.创建一个静态内部类,并在其中初始化成员常量(变量也行)static class SingletonHolder {public static final LazySingleton instance = new LazySingleton();}//3.提供方法获取静态内部类中的成员常量(或者变量public static LazySingleton getInstance() {return SingletonHolder.instance;}
}

2.测试

public class Client {public static void main(String[] args) {LazySingleton instance1 = LazySingleton.getInstance();LazySingleton instance2 = LazySingleton.getInstance();System.out.println(instance1 == instance2);}
}

两次获取的对象都是同一个

为何静态内部类中的成员变量是单例的?
因为静态代码块只有在第一次被调用的时候才会加载。

静态内部类实现单例的优点

静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。
具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,
只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。
这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么他是如何实现线程安全的?
首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的方法。
方法:这不是由程序员写的程序,而是根据代码由javac编译器生成的。
它是由类里面所有的类变量的赋值动作和静态代码块组成的。
JVM内部会保证一个类的方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,
那么只有一个线程会去执行类的方法,其他的线程都要阻塞等待,直到这个线程执行完方法。
然后执行完方法后,其他线程唤醒,但是不会再进入()方法。
也就是说同一个加载器下,一个类型只会初始化一次。

2.同步锁实现懒汉单例模式

为了保证懒汉式在多线程的环境中是线程安全的,只需要在基本的懒汉式的方法签名上加synchronized同步锁

public class LazySingleton {private LazySingleton() {}private static LazySingleton instance;//只需要在方法签名上添加一个synchronized关键字就可以保证该方法的线程安全性public static synchronized LazySingleton getInstance() {//判断instance是否为null 为null才创建,保证唯一性if (instance == null) {instance = new LazySingleton();}return instance;}
}

这样可以保证并发中懒汉单例模式的线程安全,但是由于整个方法都被同步锁锁住,导致方法效率变低,每个线程的每次请求都会阻塞等待上一个线程判断是否存在单例。

3.双重检查锁实现懒汉式单例※

使用同步锁实现懒汉式单例模式导致性能下降,为了避免,我们是可以使用双重检查锁实现懒汉式单例模式。

public class LazySingleton {private LazySingleton() {}// 1.添加volatile关键字后可以保证instance的有序性,不添加volatile关键字可能会出现空指针异常(由于jvm的指令重排)private static volatile LazySingleton instance;public static  LazySingleton getInstance() {//2.判断instance是否为null 为null就锁住后创建实例,保证安全性,双重检查锁解决了同步锁导致的多个线程进入需要排队取锁而影响性能的问题if (instance == null) {synchronized (LazySingleton.class){if(instance == null){instance = new LazySingleton();}}}return instance;}
}

实现思路:是当一个线程已经进入instance = new LazySingleton();语句后,其他线程就要等待,如果当一个线程已经成功创建单例后,其余的线程进入就只会在第一条if语句处跳出,而不需要等待锁的释放。且之后的每条线程都会直接从第一个if语句处跳出,从而保证了线程安全,大大的提高了性能。
注意点:需要添加volatile关键字修饰静态成员变量,防止空指针异常。(详情请了解jvm的指令重排)

三、破坏单例模式与预防方法

1.反射破坏单例模式

思路:通过反射获取单例类的构造器,通过构造器创建实例

public class Client {public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {//1.获取单例类的类对象Class hungerSingletonClass = HungerSingleton.class;//2.获取单例类的构造器并修改访问权限Constructor constructor = hungerSingletonClass.getDeclaredConstructor();constructor.setAccessible(true);//设置访问权限,可以访问私有构造方法//3.使用获取到的构造器创建实例HungerSingleton hungerSingleton1 = constructor.newInstance();HungerSingleton hungerSingleton2 = constructor.newInstance();//4.测试发现两个实例不是同一个对象,单例被破坏了。System.out.println(hungerSingleton1.toString());System.out.println(hungerSingleton2.toString());/*** 两次对象结果的地址不同,说明他们不是同一个对象,单例被破坏了*/}
}

预防方法

在构造方法中判断是否已经创建单例,如果已经创建,则直接手动抛出异常

public class HungerSingleton {//提供一个flag用于判断对象是否已经创建private static boolean flag = false;//1.私有构造方法private HungerSingleton() {//加锁保证多线程安全问题synchronized (HungerSingleton.class){//在无参构造中提供一个判断,如果已经创建了对象则抛出异常if(flag){throw new RuntimeException("禁止创建多个对象");}flag = true;}}//2.私有静态成员private static HungerSingleton instance;//3.静态代码块static {instance = new HungerSingleton();}//4.提供公有静态方法访问唯一对象public static HungerSingleton getInstance() {return instance;}
}

使用一个boolean类型的flag初始化为false,当flag为true是说明已经存在单例,再通过构造器创建实例就直接抛出异常。
第一次调用构造器即第一次创建单例会将flag置为true
测试一下
在这里插入图片描述
发现想利用反射创建两个实例,直接抛出了我们设置的异常。

2.反序列化破坏单例与预防方法

思路:将单例类序列化通过流存储在文件中,再通过反序列化将文件中的数据生成对象

public class Client {public static void main(String[] args) throws IOException, ClassNotFoundException {writeObjectToFile(HungerSingleton.getInstance());readObjectFromFile();readObjectFromFile();/*** 输出结果显示两个对象的地址不同,即代表两个对象不是同一个,这就破坏了单例模式了*/}//将对象序列化并存入文件public static void writeObjectToFile(HungerSingleton hungerSingleton) throws IOException {ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\d.txt")));oos.writeObject(hungerSingleton);oos.close();}//将文件中的内容反序列化为对象public static void readObjectFromFile() throws IOException, ClassNotFoundException {ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:\\d.txt")));HungerSingleton hungerSingleton = (HungerSingleton) ois.readObject();System.out.println(hungerSingleton.toString());}
}

此时两次反序列化生成的对象就是不同的两个对象。
注意:被序列化的类需要实现Serializable接口

预防方法

在单例类中添加一个readResolve()方法,在该方法内调用获取单例的静态方法

public class HungerSingleton implements Serializable {//1.私有构造方法private HungerSingleton() {}//2.静态私有成员private static HungerSingleton instance = new HungerSingleton();//3.提供一个静态的方法获取唯一实例public static HungerSingleton getInstance() {return instance;}//4.提供readResolve()方法,保证反序列化为同一个对象public Object readResolve(){return HungerSingleton.getInstance();}
}

ObjectInputStream 类中,会做一个判断,判断序列化的类中是否存在 readResolve() 方法,如果有,则会在反序列化的时候调用 readResolve() 方法。
有兴趣可以查看 ObjectInputStream 源码第2243行

四、jdk源码解析

在JDK中,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 Runtime are instance* methods and must be invoked with respect to the current runtime object.** @return  the Runtime object associated with the current*          Java application.*/public static Runtime getRuntime() {return currentRuntime;}/** Don't let anyone else instantiate this class */private Runtime() {}.............
}

可以看到Runtime也初始化了一个静态成员变量,和一个静态方法获取该成员变量,并且只有一个私有构造方法

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...