设计模式之单例模式

singleton design pattern

单例模式的概念、单例模式的结构、单例模式的优缺点、单例模式的使用场景、单例模式的实现示例、序列化和反射与单例模式、单例模式源码分析


1、单例模式的概念

  单例模式,即只能生成一个实例的模式叫做单例模式。单例模式是只有一个简单的类,没有复杂的调用和接口的设计,只要求这个类无论什么时候只能生成一个实例即可。

2、单例模式的结构

  • 私有构造方法。
  • 私有静态自身类型的属性。
  • 公有静态返回自身类型的方法。

singleton-class

3、单例模式的优缺点

3.1、优点
  • 确保所有访问者都访问同一个实例。
  • 具有一定伸缩性,单例类自己控制实例化进程。
  • 提供了对唯一实例的受控访问。
  • 允许可变数目的实例。
  • 避免对共享资源的多重占用。
  • 由于只有一个实例,所以对于需要频繁创建和销毁实例的模式来说节约了系统资源,提高了程序性能。
3.2、缺点
  • 不适用于状态多变的对象。如果同一类型的对象总是要在不同的应用场景发生变化,则会引起数据错误,因为单例模式不能保存不同场景下的对象状态。
  • 不便扩展。因为单例模式中没有抽象层,故不便扩展。
  • 单例类的职责过重,在一定程度上违反了单一职责原则。
  • 滥用单例类将带来一些负面问题。如在高并发场景下使用单例类将严重影响系统性能;若单例类实例长时间不被使用,则可可能会被系统回收,导致实例状态丢失。
  • 序列化和反射会破坏单例模式,枚举方式实现的单例模式除外。

4、单例模式的使用场景

  • 状态不变的工具类对象。
  • 频繁创建和销毁的对象。
  • 创建耗时或创建耗资源且有常用到的对象。
  • 频繁访问数据库或 IO 资源的对象。

  如日志应用、应用配置读取、通信、线程池、windows 的任务管理器、垃圾回收站等。

5、单例模式的实现示例

  单例模式一般有七种实现方式,即懒汉-线程不安全、懒汉-线程安全、饿汉、饿汉-变种、静态内部类、枚举、双重校验锁。

  • 懒汉-线程不安全

    public class SingletonOne {
    
        private static SingletonOne singletonOne;
    
        private SingletonOne() {}
    
        public static SingletonOne getInstance() {
            if (singletonOne == null) {
                singletonOne = new SingletonOne();
            }
            return singletonOne;
        }
    }
    
  • 懒汉-线程安全

    public class SingletonTwo {
    
        private static SingletonTwo singletonTwo;
    
        private SingletonTwo() {}
    
        public static synchronized SingletonTwo getInstance() {
            if (singletonTwo == null) {
                singletonTwo = new SingletonTwo();
            }
            return singletonTwo;
        }
    }
    
  • 饿汉

    public class SingletonThree {
    
        private static SingletonThree singletonThree = new SingletonThree();
    
        private SingletonThree() {}
    
        public static SingletonThree getInstance() {
            return singletonThree;
        }
    }
    
  • 饿汉-变种

    public class SingletonFour {
    
        private static SingletonFour singletonFour;
    
        static {
            singletonFour = new SingletonFour();
        }
    
        private SingletonFour() {}
    
        public static SingletonFour getInstance() {
            return singletonFour;
        }
    }
    
  • 静态内部类

    public class SingletonFive {
    
        private static class SingletonFiveHandler {
            private static final SingletonFive INSTANCE = new SingletonFive();
        }
    
        private SingletonFive() {}
    
        public static SingletonFive getInstance() {
            return SingletonFiveHandler.INSTANCE;
        }
    }
    
  • 枚举

    public enum SingletonSix {
    
        INSTANCE;
    
        SingletonSix() {}
    }
    
  • 双重校验锁

    public class SingletonSeven {
    
        private static SingletonSeven singletonSeven;
    
        private SingletonSeven() {}
    
        public static SingletonSeven getInstance() {
            if (singletonSeven == null) {
                synchronized (SingletonSeven.class) {
                    if (singletonSeven == null) {
                        singletonSeven = new SingletonSeven();
                    }
                }
            }
            return singletonSeven;
        }
    }
    

6、序列化和反射与单例模式

6.1、序列化或反射为什么会破坏单例模式

  首先,在单例模式中,单例类的构造方法是私有的,也就是说不允许外界自调用单例类的构造方法创建单例对象。其次,单例模式自始至终只能有一个实例。而在反射中,实际上是通过 Singleton.class 的方式获取到类的 Class 对象,然后获取其构造器,接着调用其构造器创建该类的实例(注意,若构造器是私有的,则可通过获取到的构造器对象,设置其访问权限,然后进行调用)。所以,反射破坏了单例模式,因为如果多次通过反射方式获取一个单例类的实例就相当于多次调用该单例类的构造器创建对象,那得倒的对象必然是不同的,必然破坏了单例模式。同时,因为反序列化的本质是反射,所以反序列化也会破坏单例模式。

6.2、如何解决反序列化或反射对单例模式的破坏
  • 解决反序列化破坏单例模式

    // 在单例类中加入此方法会解决此问题
    public Object readResolve() {
      return singletonOne;
    }
    

    java 为反序列化提供了输入流类,在反序列化时,会去判断当前类是否有 readResolve 方法,若有,则调用它。因为我们添加了 readResolve 方法,且其返回的是单例类中维护的静态单例类实例,所以就能保证每次反序列化都能得到同一个实例。同时,readResolve 方法也是 jdk 为我们提供的自定义反序列化要重写的方法。

  • 解决反射破坏单例模式

    // 单例类中维护一个表示是否是第一次创建单例对象的状态
    private static Boolean flag = false;
    
    private SingletonOne() {
      	// 加锁是为了防止在多线程环境下出现线程安全问题
    		synchronized (SingletonOne.class) {
        		if (flag) {   // 若单例对象已经存在 则抛出异常
          		throw new RuntimeException("单例类 不能创建多个对象");
        		}
        		flag = true;
      	}
    }
    

    因为反射的本质是调用类的构造方法创建对象,所以我们可以在类的构造方法中对单例类的实例的创建进行控制,保证其自始至终只能创建一个实例。

6.3、枚举方式的实现为什么不会被反射和序列化破坏
  • 首先,枚举类型为什么可以实现单例模式

    在程序中定义的枚举类型在被编译时,JVM 最终会将其转化成一个 java.lang.Enum 类,也就是枚举类型最终会被转化成类,然后被 JVM 记载,又因为 JVM 的类加载是线程安全的,也就是一个只会被加载一次,所以最后也只有一个枚举实例,所以枚举类型可以实现单例模式。

  • 其次,枚举实现的单例模式为什么不会被反序列化破坏

    这是因为枚举类型的反序列化是定制的,其序列化时是将枚举值的 name 输出到结果中,反序列化时也是直接拿到 name,所以其自始至终都是同一个实例;再者,枚举类型的序列化和反序列化的相关方法不能被重写(也就是 writeObject、readObject 方法等),所以,其实现的单例模式不能被反序列化破坏。

  • 最后,枚举实现的单例模式为什么不会被反射破坏

    这是因为在 Class 类中为枚举类型专门定义了获取其枚举值的方法,该方法实际上是直接调用了枚举类的 value 方法,从而获取到枚举值,并不是调用其构造方法,所以,其实现的单例模式不能被反射破坏。

7、单例模式的源码分析

7.1、java.lang.Runtime

  java.lang.Runtime 类使用了单例模式,且是用饿汉方式实现的。每个 java 程序都启动了一个 JVM 进程,此实例是由 JVM 实例化的,因此 ,每个 java 程序都有且仅有一个 Runtime 实例。用来在程序运行时与环境交互。

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    private static Version version;

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class {@code Runtime} are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the {@code Runtime} object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
}
7.2、java.awt.Toolkit

  java.awt.Toolkit 类使用了单例模式,且是用懒汉-线程安全的方式实现的。

public abstract class Toolkit {
  private static Toolkit toolkit;
  
  	public static synchronized Toolkit getDefaultToolkit() {
        if (toolkit == null) {
            java.security.AccessController.doPrivileged(
                    new java.security.PrivilegedAction<Void>() {
                public Void run() {
                    Class<?> cls = null;
                    String nm = System.getProperty("awt.toolkit");
                    try {
                        cls = Class.forName(nm);
                    } catch (ClassNotFoundException e) {
                        ClassLoader cl = ClassLoader.getSystemClassLoader();
                        if (cl != null) {
                            try {
                                cls = cl.loadClass(nm);
                            } catch (final ClassNotFoundException ignored) {
                                throw new AWTError("Toolkit not found: " + nm);
                            }
                        }
                    }
                    try {
                        if (cls != null) {
                            toolkit = (Toolkit)cls.getConstructor().newInstance();
                            if (GraphicsEnvironment.isHeadless()) {
                                toolkit = new HeadlessToolkit(toolkit);
                            }
                        }
                    } catch (final ReflectiveOperationException ignored) {
                        throw new AWTError("Could not create Toolkit: " + nm);
                    }
                    return null;
                }
            });
            if (!GraphicsEnvironment.isHeadless()) {
                loadAssistiveTechnologies();
            }
        }
        return toolkit;
    }
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红衣女妖仙

行行好,给点吃的吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值