• 设计模式之单例模式


    什么是单例模式

    单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创

    建型模式,它提供了一种创建对象的最佳方式。

    这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有唯一的对象实例被创建。这个类提供 了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    应用场景

    主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。

    单例模式的优点

    单例模式为系统资源的优化提供了很好的思路,频繁创建和销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。

    单例模式的实现?

    单例模式的实现常用的有两种方法,一种是懒汉式,另一种是饿汉式。

    饿汉式:在类初始化的时候就生成对象实例。

    懒汉式:在调用获取的方法时才生成对象实例。

    与饿汉式对比,懒汉式一开始需要的内存较少,但是饿汉式由于初始化是在类初始化时就完成,所以是多线程安全的,懒汉式还需要解决线程安全问题。

    饿汉式单例

    1. public Singleton {
    2. //在类的初始化时就创建对象,这里可以解决多线程访问的安全问题。
    3. private static Singleton instance = new Singleton();
    4. // 另外一种初始化,就是采用静态代码块的方式。
    5. // static {
    6. // instance = new Singleton();
    7. // }
    8. private Singleton(){}
    9. public static Singleton getInstance(){
    10. return instance;
    11. }
    12. }

    懒汉式单例

    1. public class LazySingleTon {
    2. private static LazySingleTon instance;
    3. private LazySingleTon() {
    4. }
    5. // 这里需要对多线程访问的情况进行处理,保证多线程安全,使用synchronized进行加锁操作。
    6. public static synchronized LazySingleTon getInstance() {
    7. if (instance == null) {
    8. instance = new LazySingleTon();
    9. }
    10. return instance;
    11. }
    12. }

    以上使用synchronized关键字是解决多线程情况下的并发导致的多次创建对象的问题。由于synchronized关键字加的锁属于重量级锁,对于性能有一定的影响,一般不推荐使用这种方法,下面还有双重检查加锁的方式。

    双重检查的懒汉式单例

    1. public class DoubleCheckSingleTon {
    2. // 使用volatile关键字解决指令重排的问题。
    3. private volatile static DoubleCheckSingleTon instance;
    4. private DoubleCheckSingleTon() {}
    5. public static DoubleCheckSingleTon getInstance() {
    6. // 判断是否已经完成了实例初始化
    7. if (instance == null) {
    8. // 对创建对象的过程进行加锁,以防多线程访问出现多次初始化的问题。
    9. synchronized (DoubleCheckSingleTon.class) {
    10. if (instance == null) {
    11. instance = new DoubleCheckSingleTon();
    12. }
    13. }
    14. }
    15. return instance;
    16. }
    17. }
    详细解释一下指令重排的问题

    由于没有加锁,当线程A刚执行完if判断instance为null后开始执行 instance = new DoubleCheckSingleTon();

    注意,new DoubleCheckSingleTon()这个操作在JVM层面不是一个原子操作

    具体由三步组成:

    1.为instance分配内存空间;

    2.初始化instance;

    3.将instance指向分配的内存空间。

    且这三步在JVM层面有可能发生指令重排,导致实际执行顺序可能为1-3-2,因为new操作不是原子化操作,因此,可能会出现线程A执行new DoubleCheckSingleTon()时发生指令重排的情况,导致实际执行顺序变为1-3-2,当执行完1-3还没来及执行2时(虽然还没执行2,但是对象的引用已经有了,只不过引用的是一个还没初始化的对象,此时线程B进来进行if判断后instance不为null,然后直接把线程A new到一半的对象返回了。使用volatile可以禁止指令重排,可以解决此问题。

    其它单例模式的实现

    内部静态类单例

    1. public class StaticClassSingleton {
    2. private static class SingletonHolder {
    3. private static final StaticClassSingleton INSTANCE = new StaticClassSingleton();
    4. }
    5. private StaticClassSingleton() {}
    6. public static StaticClassSingleton getInstance() {
    7. return SingletonHolder.INSTANCE;
    8. }
    9. }

    内部静态类单例可以在调用类的静态方法时,再去完成对象的初始化,这样可以在类初始化时,节省一部分内存。

    枚举单例

    1. public enum Singleton {
    2. INSTANCE;
    3. }

    枚举由于是由JVM管理,所以线程安全等问题不需要处理。

    破坏单例模式

    破坏单例模式有两种方式,分别是序列化和反射。

    序列化

    1. public HungrySingleTon implements Serializable{
    2. //在类的初始化时就创建对象,这里可以解决多线程访问的安全问题。
    3. private static HungrySingleTon instance = new Singleton();
    4. private HungrySingleTon(){}
    5. public static HungrySingleTon getInstance(){
    6. return instance;
    7. }
    8. }

    将单例对象实例序列化到磁盘上,再反序列化回来。

    1. public class TestSingleton {
    2. public static void main(String[] args) throws Exception {
    3. TestSingleton testSingleton = new TestSingleton();
    4. testSingleton.writeToDisk();
    5. testSingleton.reloadFromDisk();
    6. testSingleton.reloadFromDisk();
    7. }
    8. public void writeToDisk() throws Exception {
    9. HungrySingleTon instance = HungrySingleTon.getInstance();
    10. ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\singleton.obj"));
    11. objectOutputStream.writeObject(instance);
    12. objectOutputStream.close();
    13. }
    14. public void reloadFromDisk() throws Exception{
    15. ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\singleton.obj"));
    16. HungrySingleTon singleTon = (HungrySingleTon) objectInputStream.readObject();
    17. System.out.println(singleTon);
    18. }
    19. }
    20. //****************调用结果如下**************
    21. // com.code.designpattern.HungrySingleTon@182decdb
    22. // com.code.designpattern.HungrySingleTon@26f0a63f
    23. // Process finished with exit code 0

    以上调用结果显示,经过序列化和反序列化后,单例模式的结果被破坏了。

    解决办法

    在单例类中添加一个加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,

    就返回这个方法的值,如果没有定义,则返回新new出来的对象。

    1. public HungrySingleTon implements Serializable{
    2. //在类的初始化时就创建对象,这里可以解决多线程访问的安全问题。
    3. private static HungrySingleTon instance = new Singleton();
    4. private HungrySingleTon(){}
    5. public static HungrySingleTon getInstance(){
    6. return instance;
    7. }
    8. // 添加readResolve方法解决破坏单例的问题。
    9. public Object readResolve() {
    10. return instance;
    11. }
    12. }
    readResolve方法原理

    ObjectInputStream类中的readObject方法

    1. public final Object readObject() throws IOException,
    2. ClassNotFoundException{
    3. ...
    4. // if nested read, passHandle contains handle of enclosing object
    5. int outerHandle = passHandle;
    6. try {
    7. Object obj = readObject0(false);//重点查看readObject0方法
    8. .....
    9. }
    10. private Object readObject0(boolean unshared) throws IOException {
    11. ...
    12. try {
    13. switch (tc) {
    14. ...
    15. case TC_OBJECT:
    16. return checkResolve(readOrdinaryObject(unshared));//重点
    17. //查看readOrdinaryObject方法
    18. ...
    19. }
    20. } finally {
    21. depth--;
    22. bin.setBlockDataMode(oldMode);
    23. }
    24. }
    25. private Object readOrdinaryObject(boolean unshared) throws IOException
    26. {
    27. ...
    28. //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例
    29. //类,
    30. obj = desc.isInstantiable() ? desc.newInstance() : null;
    31. ...
    32. // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod()
    33. //方法执行结果为true
    34. if (obj != null && handles.lookupException(passHandle) == null &&
    35. desc.hasReadResolveMethod()) {
    36. // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变
    37. //
    38. // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们
    39. // 定义的readResolve方法,所以返回的是同一个对象。
    40. Object rep = desc.invokeReadResolve(obj);
    41. ...
    42. }
    43. return obj;
    44. }

    反射

    反射调用单例,破坏单例模式。

    1. public class TestSingletonQuestion {
    2. public static void main(String[] args) throws Exception {
    3. Constructor<HungrySingleTon> declaredConstructor = HungrySingleTon.class.getDeclaredConstructor();
    4. declaredConstructor.setAccessible(true);
    5. HungrySingleTon instance = declaredConstructor.newInstance();
    6. HungrySingleTon instance1 = declaredConstructor.newInstance();
    7. System.out.println(instance1 == instance);
    8. }
    9. }
    解决反射破坏单例模式的办法

    可以通过在私有的构造函数中判断当前对象是否已经初始化过,也可以添加一个静态的标志变量,用以表示是否已经初始化过对象实例,如果初始化过,就直接抛出异常,达到单例构造器禁止反射多次调用的方式。

    下面写出标志变量的方法

    1. package com.code.designpattern;
    2. import java.io.Serializable;
    3. public class HungrySingleTon implements Serializable {
    4. private static HungrySingleTon instance = new HungrySingleTon();
    5. // 静态标志变量,用于标识是否初始化过实例对象。
    6. private static boolean flag = false;
    7. private HungrySingleTon() {
    8. if (flag) {
    9. throw new RuntimeException("单例构造器禁止反射调用");
    10. }
    11. flag = true;
    12. }
    13. public static HungrySingleTon getInstance() {
    14. return instance;
    15. }
    16. public static void main(String[] args) {
    17. HungrySingleTon instance1 = HungrySingleTon.getInstance();
    18. HungrySingleTon instance2 = HungrySingleTon.getInstance();
    19. System.out.println(instance1 == instance2);
    20. }
    21. public Object readResolve() {
    22. return instance;
    23. }
    24. }
  • 相关阅读:
    Spring Cloud Alibaba微服务第28章之Harbor安装以及镜像推送
    js基础API初学
    电脑内存不足怎么办
    莞中 2022暑假训练题04:树型DP
    快速掌握数据分析思路
    LeetCode --- 1436. Destination City 解题报告
    在get对象属性时发现没有get对应的方法
    C51--蓝牙HC-08
    uni-app_消息推送_华为厂商_unipush离线消息推送
    SpringBoot读取yml配置文件
  • 原文地址:https://blog.csdn.net/qq_24054661/article/details/133864261