赞
踩
1 ) 定义
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。
单例模式也比较好理解,比如一个人一生当中只能有一个真实的身份证号,一个国家只有一个政府,类似的场景都是属于单例模式。
2 ) 使用单例模式要做的两件事
3 ) 单例模式结构
在类加载期间初始化静态实例,保证 instance 实例的创建是线程安全的 ( 实例在类加载时实例化,有JVM保证线程安全).
特点: 不支持延迟加载实例(懒加载) , 此中方式类加载比较慢,但是获取实例对象比较快
问题: 该对象足够大的话,而一直没有使用就会造成内存的浪费。
- public class Singleton_01 {
-
- //1. 私有构造方法
- private Singleton_01(){
-
- }
-
- //2. 在本类中创建私有静态的全局对象
- private static Singleton_01 instance = new Singleton_01();
-
-
- //3. 提供一个全局访问点,供外部获取单例对象
- public static Singleton_01 getInstance(){
-
- return instance;
- }
-
- }

此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如果是多线程情况,会出现线程安全问题.
- public class Singleton_02 {
-
- private Singleton_02(){
-
- }
-
-
- private static Singleton_02 instance;
-
-
- public static Singleton_02 getInstance(){
-
- if(instance == null){
-
- instance = new Singleton_02();
- }
- return instance;
- }
-
- }

假设在单例类被实例化之前,有两个线程同时在获取单例对象,线程A在执行完if (instance == null) 后,线程调度机制将 CPU 资源分配给线程B,此时线程B在执行 if (instance == null)时也发现单例类还没有被实例化,这样就会导致单例类被实例化两次。为了防止这种情况发生,需要对 getInstance() 方法同步处理。改进后的懒汉模式.
原理: 使用同步锁 synchronized锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建
- public class Singleton_03 {
-
- //1. 私有构造方法
- private Singleton_03(){
-
- }
-
- //2. 在本类中创建私有静态的全局对象
- private static Singleton_03 instance;
-
-
- //3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性
- public static synchronized Singleton_03 getInstance(){
-
- if(instance == null){
- instance = new Singleton_03();
- }
- return instance;
- }
-
- }

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
实现步骤:
- /**
- * 单例模式-双重校验
- * @author spikeCong
- * @date 2022/9/5
- **/
- public class Singleton_04 {
-
- //使用 volatile保证变量的可见性
- private volatile static Singleton_04 instance = null;
-
- private Singleton_04(){
- }
-
- //对外提供静态方法获取对象
- public static Singleton_04 getInstance(){
- //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
- if(instance == null){
- synchronized (Singleton_04.class){
- //抢到锁之后再次进行判断是否为null
- if(instance == null){
- instance = new Singleton_04();
- }
- }
- }
-
- return instance;
- }
- }

使用了 volatile 之后,相当于是表明了该字段的更新可能是在其他线程中发生的,因此应确保在读取另一个线程写入的值时,可以顺利执行接下来所需的操作。在 JDK 5 以及后续版本所使用的 JMM 中,在使用了 volatile 后,会一定程度禁止相关语句的重排序,从而避免了上述由于重排序所导致的读取到不完整对象的问题的发生。
- public class Singleton_05 {
-
- private static class SingletonHandler{
- private static Singleton_05 instance = new Singleton_05();
- }
-
- private Singleton_05(){}
-
- public static Singleton_05 getInstance(){
- return SingletonHandler.instance;
- }
- }
反射的概念: JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
反射技术过于强大,它可以通过setAccessible()来修改构造器,字段,方法的可见性。单例模式的构造方法是私有的,如果将其可见性设为public,那么将无法控制对象的创建。
- public class Test_Reflect {
-
- public static void main(String[] args) {
-
- try {
-
- //反射中,欲获取一个类或者调用某个类的方法,首先要获取到该类的Class 对象。
- Class<Singleton_05> clazz = Singleton_05.class;
-
- //getDeclaredXxx: 不受权限控制的获取类的成员.
- Constructor c = clazz.getDeclaredConstructor(null);
-
- //设置为true,就可以对类中的私有成员进行操作了
- c.setAccessible(true);
-
- Object instance1 = c.newInstance();
- Object instance2 = c.newInstance();
-
- System.out.println(instance1 == instance2);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }

解决方法之一: 在单例类的构造方法中 添加判断 instance != null 时,直接抛出异常
- public class Singleton_05 {
-
- private static class SingletonHandler{
- private static Singleton_05 instance = new Singleton_05();
- }
-
- private Singleton_05(){
- if(SingletonHandler.instance != null){
- throw new RuntimeException("不允许非法访问!");
- }
- }
-
- public static Singleton_05 getInstance(){
- return SingletonHandler.instance;
- }
- }

上面的这种方式使代码简洁性遭到破坏,设计不够优雅.
- /**
- * 序列化对单例的破坏
- * @author spikeCong
- * @date 2022/9/6
- **/
- public class Test_Serializable {
-
- @Test
- public void test() throws Exception{
-
- //序列化对象输出流
- ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile.obj"));
- oos.writeObject(Singleton.getInstance());
-
- //序列化对象输入流
- File file = new File("tempFile.obj");
- ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
- Singleton Singleton = (Singleton) ois.readObject();
-
- System.out.println(Singleton);
- System.out.println(Singleton.getInstance());
-
- //判断是否是同一个对象
- System.out.println(Singleton.getInstance() == Singleton);//false
-
- }
- }
-
-
- /**
- * 单例类实现序列化接口
- */
- class Singleton implements Serializable {
-
- 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;
- }
-
- }

输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
在 Java 的序列化过程中,反序列化会通过反射调用无参数的构造方法来创建一个新的对象实例。如果我们想要控制反序列化的结果,尤其是在单例模式(Singleton)中,我们可以通过实现 readResolve
方法来解决这个问题。
以下是 readResolve
方法的作用以及为什么重写这个方法可以解决单例问题的原因:
readResolve
方法readResolve
是 Java 序列化机制中的一个特殊方法。如果在反序列化过程中找到这个方法,序列化机制就会使用 readResolve
方法返回的对象,而不是直接使用反射创建的对象。通过这个方法,我们可以确保在反序列化过程中返回的是我们想要的单例对象。
以下是一个简单的单例类实现,展示了如何使用 readResolve
方法:
- import java.io.Serializable;
-
- public class Singleton implements Serializable {
- private static final long serialVersionUID = 1L;
- private static final Singleton INSTANCE = new Singleton();
-
- private Singleton() {
- // 私有构造方法防止外部实例化
- }
-
- public static Singleton getInstance() {
- return INSTANCE;
- }
-
- // 确保反序列化时返回的是同一个实例
- private Object readResolve() {
- return INSTANCE;
- }
- }

在这个例子中,Singleton
类实现了 Serializable
接口,并且包含一个私有的 readResolve
方法。这个方法确保在反序列化过程中总是返回同一个 INSTANCE
,从而保证单例的约束。
总结
通过重写 readResolve
方法,我们可以在反序列化过程中控制返回的对象,从而确保单例模式的唯一性。这是因为序列化机制在反序列化时会调用 readResolve
方法来获取最终返回的对象,而不是直接使用反射创建的对象实例。
- public enum Singleton_06{
-
- INSTANCE;
-
- private Object data;
-
- public Object getData() {
- return data;
- }
-
- public void setData(Object data) {
- this.data = data;
- }
-
- public static Singleton_06 getInstance(){
-
- return INSTANCE;
- }
- }

线程安全:枚举类型在 Java 中本质上是线程安全的,JVM 保证了枚举值在任何情况下都是唯一的且只有一个实例。这避免了我们手动实现单例时需要进行同步(如使用 synchronized
关键字或双重检查锁定)来保证线程安全的问题。
防止反序列化破坏单例:通过使用枚举来实现单例,Java 序列化机制能够自动保证唯一性,不需要我们手动实现 readResolve
方法来防止反序列化时创建新的实例。
防止反射攻击:枚举类型还能够防止通过反射机制来创建新的实例。尝试通过反射调用枚举类的构造方法会抛出异常,这在其他单例实现中需要额外的保护措施来防止。
1 ) 单例的定义
单例设计模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。
2 ) 单例的实现
饿汉式
懒汉式
双重检测
静态内部类
枚举方式
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。