单例模式单例模式精讲(上)

Posted 写Bug的渣渣高

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式单例模式精讲(上)相关的知识,希望对你有一定的参考价值。

⭐️引言⭐️

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

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

单例模式(下)
- 容器单例
- 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;

以上是关于单例模式单例模式精讲(上)的主要内容,如果未能解决你的问题,请参考以下文章

设计模式课程 设计模式精讲 8-9 单例设计模式-容器模式

单例模式(万字长文精讲)

设计模式课程 设计模式精讲 8-11 单例模式源码解析(jdk+spring+mybaties)

单例模式如何征服面试官(万字长文精讲)

python中的单例设计模式

设计模式课程 设计模式精讲 8-6 单例设计模式-序列化破坏单例模式原理解析及解决方案