【设计模式】单例模式精讲(上)

⭐️引言⭐️

我将单例模式分为两部分
单例模式(上):

  • 为什么需要单例模式+案例讲解
  • 实现单例模式
  • 破坏单例模式的情况及其解决方法
    • 反射
    • 序列化

单例模式(下)
- 容器单例
- ThreadLocal线程单例
- 使用单例处理资源访问冲突
- 使用单例表示全局唯一类




⭐️为什么需要单例模式⭐️

防止重复创建对象而损耗性能
在此情况下使用单例模式,好处有这些:

1.在内存中只有一个对象,节省内存空间。
2避免频繁的建立销毁对象,能够提升性能。
3.避免对共享资源的多重占用。
4.能够全局访问。

比如说我们想使用一个工具类,在普通情况下,工具类中方法使用静态方法即可,若在特殊情况下,比如多数据源,我们可以使用单例模式来开发这个工具类。
此时,假如有很多类需要这个工具类,如果是普通的类,那么需要频繁的创建对象,使用方法,销毁对象,会占用内存资源。
实际例子:
你是一个天天写检讨书的同学,你如何省时省力的去写检讨,那就是建立一份万能模板,每次写检讨,就直接拿到这份万能检讨即可。在这个过程中:一份万能模板即可,不需要新的,每次使用这个模板即可,使用的结果就是省时省力。

大部分情况下,在没有业务需求的情况下,若使用单例模式,我们的目的一般是用来节省内存(下一个文章介绍其他好处),下面我们先来说如何实现单例模式,以及一些可能会破坏单例模式的情况




⭐️单例模式代码编写⭐️

单例模式主要分为两种,饿汉式和懒汉式。
区别:
- 饿汉式:在类被加载的时候,因为使用到了static修饰符,然后又创建了自己的实例,所以该实例会在类加载时直接创建。—占用系统内存
- 懒汉式:在调用获取该对象方法的时候,才创建对象。(我们把这种行为叫做延迟加载)

⭐️懒汉式1.0 版本⭐️

思路:
判断当前
目标:基本实现懒汉式单例模式(有缺陷)

//延迟加载
class LazySingleton{
    private static LazySingleton instance;
    public LazySingleton getInstance(){
        if (instance==null){
            instance=new LazySingleton();
        }
        return instance;
    }
}

⭐️懒汉式2.0 版本⭐️

写代码,在追求正确的情况下,还得追求适用性。既然这么说了,V1.0肯定有缺陷,缺陷就在于:不适用多线程环境

解释:

getInstance中有三行语句,假如现在有两个线程,一个正在创建对象,但是没有创建完毕(instance 为 null),另一个对象也进入了该线程,然后因为另一个线程创建对象未完成,导致了instance 为null, 然后又创建了一个对象

流程:
时刻1:线程1正在创建对象,但是未创建完成 —>instance == null
时刻1:线程2正在判断instance是否未null —>instance ==null 成立 —>创建对象
时刻2: 线程1创建好对象
时刻3:线程2创建好对象

问题出在哪里?

根据上面的解释,很容易发现,问题出在if判断语句和创建对象,这几行代码不是一个整体,所以在一个线程运行的时候,另一个线程可能乘虚而入。俗话说的好,苍蝇不叮无缝的蛋,我们把这几行代码变成一个整体。

有一个关键词:synchronized,使用了这个关键词后只能有一个线程能进入该方法,那么整个方法都是一个整体,自然内部是不会有问题的。

class LazySingleton{
    private static LazySingleton instance;
//  给整个方法加上锁,但是对性能影响比较大
  使用多线程debug
    public synchronized LazySingleton getInstance(){
        if (instance==null){
            instance=new LazySingleton();
        }
        return instance;
    }
}

⭐️懒汉式3.0 版本⭐️

使用synchronized会导致这个方法的性能较差,我们可以考虑把synchronized放到内部

思考:我们可以使用 synchronized (LazySingleton.class)来锁定if语句

大家看看下面的代码好不好

    public LazySingleton getInstance(){
          synchronized (LazySingleton.class){
              if (instance==null){
                  instance=new LazySingleton();
              }
          }
        return instance;
    }

其实是不够好的,因为我们假如把整个if语句锁起来,那么和把锁放在方法上区别并不够大,毕竟整个方法里面也只有一个if和return,我们已经把if全包起来了。
假如实例创建完毕,我们线程1,2再次获取,此时也仅有一个能先判断,另一个等着。
**问题出在哪里?:**出在即使对象存在,线程1,2无法同时进入判断语句。

解决方法: 在最外层再加入一层if

升级版

    public LazySingleton getInstance(){
    //		先判断是否为null ,如果空,那么就进入,因为if和创建对象被加锁,所以一个时刻只能有一个线程运行if语句
    //	如果不为空,那么不需要等锁,直接返回
        if (instance==null){
            synchronized (LazySingleton.class){
                if (instance==null){
                    instance=new LazySingleton();
                }
            }
        }
        return instance;
    }

回顾

问题1:if语句和创建实例语句不是一个整体即不是一个原子操作
出现时刻:线程1正在创建而未创建成功,但是线程2已经判断出instance为null,然后准备创建对象
解决1:给方法加上synchronized
优化:给if语句套上synchronized(但是即使对象存在,线程不能同时进入if语句)
优化2:doublecheck双重检查,再套一个if语句

注意点1:
懒汉式的实例不能用final修饰—>不是在类加载的时候就初始化好

⭐️ 饿汉式 ⭐️

该方式创建的单例是在类加载的时候就已经创建了实例。
饿汉式稍微简单点:但是记住,如果该单例没有使用,那么会浪费内存!!!

class HungrySingleton{
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
//        当强制使用反射来构造的时候,也无法构造出另一个类
        if (instance!=null){
            throw new RuntimeException("单例不允许多个实例");
        }
    }

    public static HungrySingleton getInstance(){
        return instance;
    }
}

⭐️ 有哪些情况会破坏单例模式 ⭐️

反射破坏单例

要搞清楚原因就知道反射是如何破坏单例的:

首先我们要了解到反射是如何破坏单例的—>通过获得单例对象的构造器然后暴力构造一个对象(即使构造器为private)

class HungrySingleton implements Serializable {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
//        当强制使用反射来构造的时候,也无法构造出另一个类
        if (instance!=null){
            throw new RuntimeException("单例不允许多个实例");
        }
    }

    public static HungrySingleton getInstance(){
        return instance;
    }

    //为什么要这么写
    private Object readResolve(){
        return instance;
    }
}
public static void testInvoke() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
		  Class objectClass = HungrySingleton.class;
		  Constructor constructor=objectClass.getDeclaredConstructor();
		  constructor.setAccessible(true);
		  HungrySingleton instance = HungrySingleton.getInstance();
		  HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
		  System.out.println(instance);
		  System.out.println(newInstance);
}
如何解决:

在构造器中判断并抛出异常

思考:

为什么我们使用了饿汉式举例,却没有将懒汉式。因为这种方法只对类加载时就创建好的单例模式有用。(静态内部类单例模式也可以这样使用)

验证懒汉式无法使用此种方法来防止反射

public class LazySingleTon {
    private static LazySingleTon lazySingleTon = null;

    private LazySingleTon(){
        if (lazySingleTon!=null){
            throw new RuntimeException("单例不允许多个实例");
        }
    }

    public static LazySingleTon getInstance(){
        if (lazySingleTon == null){
            lazySingleTon = new LazySingleTon();
        }
        return lazySingleTon;
    }

}


test方法

    public static void testLazyInvoke() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        和之前的有所不同。之前的内部类和饿汉式,都是直接创建出来的
//        但是这里的懒汉式,是在需要的时候才创建出来的,所以我们使用
//        反射获得的结果会有所不同,假如先反射再 getInstance ,那么
//        应该是不同的,假如先 getInstance 再反射,那么无法反射成功
        Class objectClass = LazySingleTon.class;
        Constructor constructor=objectClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleTon instance = LazySingleTon.getInstance();

        LazySingleTon newInstance = (LazySingleTon) constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
    }

结果:
Exception in thread “main” java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.ggzx.design.pattern.singleton.Test.testLazyInvoke(Test.java:89)
at com.ggzx.design.pattern.singleton.Test.main(Test.java:17)
Caused by: java.lang.RuntimeException: 单例不允许多个实例
at com.ggzx.design.pattern.singleton.LazySingleTon.(LazySingleTon.java:8)
… 6 more

但是,这种情况有例外,上面的是先通过getInstance获得单例模式提供的实例,然后再通过反射这种”非法“手段获取对象,此时可以正常报错。但是只要互换代码后,结果就不一样了。
我们先通过”非法“手段获取单例,再通过正常方法获得时,就获得了两个对象.
原因也很明显:因为反射获得对象后, lazySingleTon仍然为null
在这里插入图片描述

        LazySingleTon newInstance = (LazySingleTon) constructor.newInstance();

        LazySingleTon instance = LazySingleTon.getInstance();
       

在这里插入图片描述

序列化

序列化:把对象转换为字节序列的过程称为对象的序列化。
反序列化:把字节序列恢复为对象的过程称为对象的反序列化

    public static void testSerializable() throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("singleTone"));
        objectOutputStream.writeObject(instance);

        File file = new File("singleTone");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) objectInputStream.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

结果:
com.ggzx.design.pattern.singleton.HungrySingleton@14ae5a5
com.ggzx.design.pattern.singleton.HungrySingleton@378bf509
false

可以发现,经过序列化之后,就不被认定为同一个对象了

先说如解决办法
只需要增加一个redResolve方法即可


class HungrySingleton implements Serializable {
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){
//        当强制使用反射来构造的时候,也无法构造出另一个类
        if (instance!=null){
            throw new RuntimeException("单例不允许多个实例");
        }
    }

    public static HungrySingleton getInstance(){
        return instance;
    }

    //为什么要这么写
    private Object readResolve(){
        return instance;
    }
}

为什么
先查看readObject方法
在这里插入图片描述
找到read0方法,这里是读取序列化对象的地方在这里插入图片描述我们的类继承自Object类,找到这里的代码,进入readOrdinaryObject
在这里插入图片描述
下面才是关键代码
在这里插入图片描述

//obj就是返回的对象
ObjectStreamClass desc = readClassDesc(false);
//如果可以序列化,就通过反射创建对象,通过反射创建的对象是不同的
obj = desc.isInstantiable() ? desc.newInstance() : null;

isInstantiable方法

    /** serialization-appropriate constructor, or null if none */
    private Constructor<?> cons;
// con是构造器,如果这个对象支持序列化,那么上方的desc.isInstantiable()为真,就通过反射创建一个对象
  boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }

上面了解了破坏单例模式的原因,下面来找解决方法
来从源代码中找到答案

// 注意其中的hasReadResloveMethod
  if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

hasReadResloveMethod方法源代码

    /**
     * Returns true if represented class is serializable or externalizable and
     * defines a conformant readResolve method.  Otherwise, returns false.
     * 假如有一个readResolve方法,就返回真,
     */
    boolean hasReadResolveMethod() {
        requireInitialized();
        return (readResolveMethod != null);
    }

在这里插入图片描述
如果存在reaReslove方法,那么就通过反射来调用readReslove方法
在这里插入图片描述

private Object readResolve(){
   return instance;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

渣渣高不会写Java

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值