设计模式之单例模式

本文详细介绍了单例模式的定义、优点、缺点,包括内存优化、线程安全问题,以及饿汉式、懒汉式、静态内部类、枚举和反射实现方式。同时讨论了序列化和反射如何破坏单例模式,以及相应的解决方案。
摘要由CSDN通过智能技术生成

1、定义

      确保一个类只有一个实例,且由类自行实例化并向整个系统提供这个实例
      从定义可以看出来单例模式一个很重要的操作是单例类的构造方法私有化

2、单例模式优点

      2.1)由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要

               频繁的创建和销毁时,而且创建或销毁时性能又无法优化,此时单例模式的优

               势就非常明显

      2.2)由于单例模式只生成一个实例,所以减少了系统的性能开销;

               在JAVA EE中使用单例模式时需要注意JVM垃圾回收机制。

      2.3)单例模式可以避免对资源的多重暂用,例如一个写文件动作,由于只有一个实例

               存在于内存中,就避免了对同一资源文件的同时写操作

      2.4)单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计

               一个类负责所有数据表的映射处理。

3、单例模式缺点

      3.1)单例模式一般没有接口,扩展困难

      3.2)单例模式对测试不利,在并行开发环境中,若单例模式没有完成是不能进行测试的

      3.3)单例模式与单一职责原则冲突

4、单例模式使用场景

     在系统中,若要求一个类只有一个对象,如果出现多个对象就会出现异常,则可以采用单例

     模式,具体场景如下:

            4.1)要求生成唯一序列号的场景

            4.2)在一个项目中需要一个共享访问点或共享数据,如统计一个网站的访问数量的计数器

            4.3)创建一个对象需要消耗过多的资源,如访问io和数据库等资源

            4.4)需要定义大量的静态常量或静态方法的环境,可以使用单例模式(也可以static的

                     方式),如工具类、常量类

5、饿汉式实现单例模式

      单例模式在实现上可以分为饿汉式和懒汉式;

      饿汉式在jvm加载类时就会创建全局的实例,实例的引用用final修饰,表示实例创建成功

       后就不在修改;
       饿汉式天生是线程安全的,由jvm控制线程安全

        饿汉式代码如下:

         

/*******************************************************
 *
 * 饿汉式单例模式:
 *     饿汉式单例,在jvm加载类时就会创建全局的实例,实例的引用用final
 *     修饰,表示实例创建成功后就不在修改;
 *     饿汉式天生是线程安全(由jvm控制线程安全)的
 * 饿汉式单例缺点:
 *     在类加载时就会创建对象,如果一个项目中有大量的单例模式(饿汉式),那么
 *     在项目启动时就会创建大量的对象,这对电脑的内存也是一个很大的压力
 *
 * @author lbf
 * @date 2024-03-17 09:39
 *******************************************************/
public class SingleDem01 {

    private static final SingleDem01 single = new SingleDem01();

    private SingleDem01(){

    }

    public static SingleDem01  getSingle(){

        return single;
    }
}

6、懒汉式单例实现

      懒汉式单例模式在第一次使用时才实例化对象;

      懒汉式代码如下:

      代码6-1:

/*******************************************************
 * 懒汉单例模式(一)
 * 懒汉单例模式即在使用的时候才创建实例
 *
 *
 * @author lbf
 * @date 2024-03-17 09:52
 *******************************************************/
public class SingleIdler01 {

    private static SingleIdler01 single = null;


    private SingleIdler01(){}

    /**
     * 这种方式下创建的单例不是线程安全的,在多线程场景下可能会创建多个对象
     * @return
     */
    public static SingleIdler01 getInstance(){

        if(single == null){
            single = new SingleIdler01();
        }

        return single;
    }
}

     上边代码6-1 是非线程安全的,在并发环境中可能会创建多个对象,当线程A 执行到  

single = new SingleIdler01(); 正在创建对象时(注意:此时对象还没创建完成,single还

是等于null),线程B执行到single==null 返回true,接着线程B再次创建了对象。

解决线程安全最简单的方式是加锁,代码如 6-2 所示:

代码 6-2 :

public class SingleIdler01 {

    private static SingleIdler01 single = null;


    private SingleIdler01(){}

    /**
     * 这种方式下创建的单例不是线程安全的,在多线程场景下可能会创建多个对象
     * @return
     */
    public synchronized static SingleIdler01 getInstance(){

        if(single == null){
            single = new SingleIdler01();
        }

        return single;
    }
}

 上边代码6-2 虽然解决了多线程环境下的线程安全问题,但也引入了新的的问题,

方法getInstance() 主要是读取操作,加上synchronized锁后每次只能有一个线程

调用getInstance(),这就很大程度上降低了系统的性能。

针对代码6-2 的问题可以使用 “双重检查机制+锁” 的方式解决,在判断需要创建对象后

,对创建对象的代码块进行加锁,减小锁的粒度,读取 single 时不需要获取锁;

代码如6-3 所示:

代码6-3

public class SingleIdler01 {

    private volatile static SingleIdler01 single = null;


    private SingleIdler01(){}

   

    /**
     * 方案二:
     *   双重检查机制+锁 的方式来解决懒汉模式的线程安全
     *   1)变量 single 线程可见的,即用 volatile 修饰;
     *      若 变量 single不是线程可见的,由于jvm指令重排序的原因,在并发环境下当线程A正在锁内 
     *      创建对象,此时由于指令重排序把对象的地址赋值给 single的代码先执行,注意但此时对象创 
     *      建并没完成,single指向的对象不是完成的;
     *      正好此时线程B当执行到第一步判断,single != null,则直接返回single,这时的single不 
     *      是完整的对象,从而导致Null指针异常
     *   2)第一次检查后对代码块加锁,减小锁的粒度
     *   3)在锁中创建对象之前再检查一次对象是否已经创建
     */
    public static SingleIdler01 getInstance(){

        if(single == null){//第一次检查
            synchronized (SingleIdler01.class){
                if(single == null){//第二次检查
                    single = new SingleIdler01();
                }
            }
        }

        return single;
    }
}

7、使用静态内部类实现懒汉单例模式

     静态内部类实现的单例模式既实现了线程安全,又避免了synchronized同步带来的性能影响。        当getInstance方法第一次被调用的时候,它第一次读取 SingletonHolder.handler,

   导致 SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静

   态域,从而创建SingleStaticClass的实例,由于是静态的域,因此只会在虚拟机装载类的

   时候初始化一次,并由虚拟机来保证它的线程安全性。
 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个静态域的访问,

    因此延迟初始化并没有增加任何访问成本。

   代码如下:

public class SingleStaticClass {

    private static SingleStaticClass single = null;


    private SingleStaticClass(){}

    /**
     * 调用静态内部类来实例化SingleStaticClass
     * @return
     */
    public static SingleStaticClass getInstance(){


        return SingleHandler.handler;
    }


    /**
     * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
     * 没有绑定关系,而且只有被调用到时才会装载,从而实现了延迟加载
     */
    private static class SingleHandler{
        /**
         * 静态成员,类加载时调用,由JVM保证线程安全
         */
        private static SingleStaticClass handler = new SingleStaticClass();
    }
}

8、使用枚举实现懒汉单例模式

     枚举方式实现的懒汉单例也是线程安全的,并且只会加载一次;而且这种方式是唯一一种

     不会被破坏的单例模式实现方式,像上边几种单例的实现方式都可以通过反射创建对象的

     方式破坏单例。

     代码如下:

public class SingleEnum {

    private SingleEnum(){}

    /**
     * 获取单例对象
     *
     * @return
     */
    public static SingleEnum getInstance(){
        return Singleton.INSTANCE.getInstance();
    }

    /**
     * 枚举
     * 枚举是线程安全的,且只会加载一次;
     * 而且枚举实现的单例不会被java反射破坏
     *
     */
    private enum Singleton{
        INSTANCE;

        private SingleEnum instance;

        /**
         * 在加载的时候执行,创建对象
         */
        Singleton(){
            instance = new SingleEnum();
        }

        private SingleEnum getInstance(){
            return instance;
        }
    }
}

9、反射对单例的破坏

     在反射中,可以通过 setAccessible(true) 方法设置类中私有属性和方法变为外部可访问,

     破坏类的私有问题。

     在单例类中通过方法 setAccessible(true) 设置私有构造方法为可访问,这样在外边可以直接

     构造方法来创建对象,这就破坏了单例的特性。

     下面以 上边“静态内部类实现单例” 的类 SingleStaticClass 来验证反射对单例的破坏,

      代码如下:

public class ReflectSingle {

    public static void main(String[] args) {
        /*
         * 以静态内部类实现单例的方式来验证反射对单例的破坏
         */
        Class<SingleStaticClass> clazz = SingleStaticClass.class;
        //通过反射获取 SingleStaticClass 的无参构造方法,并手动设置为可访问
        try {
            Constructor<SingleStaticClass> constructor = clazz.getDeclaredConstructor();
            //设置该方法可以访问
            //即使在 SingleStaticClass 类中构造方法 constructor 是私有的,setAccessible设置为true也是可以访问的
            constructor.setAccessible(true);
            //通过构造方法创建对象
            SingleStaticClass object1 = constructor.newInstance();
            SingleStaticClass object2 = constructor.newInstance();
            System.out.println(object1 == object2);

        } catch (Exception e) {
           e.printStackTrace();
        }
    }
}

      可以在类 SingleStaticClass 的构造方法中添加上判断对象是否已经创建,若已经创建,则抛出

      异常,终止对象的创建。代码如下:

public class SingleStaticClass {

    private static SingleStaticClass single = null;


    private SingleStaticClass(){
        //解决反射对单例的破坏
            /**
             * 在 SingleStaticClass 的构造方法中添加一个判断,若对象已经实例化,则抛出异常
             */
        if(SingleHandler.handler != null){
            throw new RuntimeException("单例已存在,请直接调用 getInstance() 方法获取单例对象");
        }
    }


    //。。。。。。。。。。。。。 其他代码同上,这里省略 。。。。。。。。。。。。。。
}

10、序列化对单例的破坏

10.1、以双重检查机制单例为例,先校验序列化是否破坏了单例模式

         步骤如下:

                1)单例类Single实现序列化接口Serializable,表示单例类可以序列化

                2)把单例类对象写入到一个文件中,以文件的方式写入

                3)从文件中把单例对象读取出来,记为single,以文件的方式读取

                4)调用单例类的 getInstance() 方法,获取单例对象singleGet

                5)比较对象 single和singleGet 是否相等,若不相等则说明单例模式遭到了破坏

            示例代码如下:

             

/*******************************************************
 * 序列化对单例的破坏
 *
 *******************************************************/
public class SerializeSingle {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        //序列化对象输出流,将Singleton 对象写入到文件 tempFile.obj 中
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile.obj"));
        oos.writeObject(Singleton.getInstance());

        //序列化对象输入流,从文件tempFile.obj 中读取刚刚写入的Singleton 对象
        File file = new File("tempFile.obj");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        //以对象的形式读取数据
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton);

        //再获取单例类 Singleton 的对象,比较 2个对象是否相等
        Singleton singletonGet = Singleton.getInstance();
        System.out.println(singletonGet);

        //若2个对象相等,则表示单例模式没有被破坏,否则表示序列化破坏了单例模式
        System.out.println(singleton == singletonGet); //运行结果输出false


    }
}

/**
 * 序列化单例类
 */
class Singleton implements Serializable {

    private volatile static Singleton single = null;

    private Singleton(){

    }

    //使用双重检查机制实现单例模式
    public static Singleton getInstance(){

        if(single == null){
            synchronized (Singleton.class){
                if(single == null){
                    single = new Singleton();
                }
            }
        }
        return single;
    }

}

10.2、解决序列化对单例的破坏

          要解决序列化对单例的破坏也很简单,只需要在上边单例类Singleton 中定义方法

          readResolve()并让该方法返回单例对象,就能解决序列化对单例的破坏,

          注意:方法名称 readResolve() 不能被改变

          Singleton 代码如下:

/**
 * 序列化单例类
 */
class Singleton implements Serializable {

    private volatile static Singleton single = null;

    private Singleton(){

    }

    //使用双重检查机制实现单例模式
    public static Singleton getInstance(){

        if(single == null){
            synchronized (Singleton.class){
                if(single == null){
                    single = new Singleton();
                }
            }
        }
        return single;
    }

    /**
     * 只要在单例类Singleton 定义readResolve()方法,就可以解决序列化对单例模式的破坏
     *
     * 为什么加上这个方法就可以解决序列化对单例的破坏?
     *
     * @return
     */
    private Object readResolve(){

        return single;
    }
}

10.3、分析为甚了Singleton 定义了 方法 readResolve() 就能解决序列化对单例的破坏?

         1、 序列化破坏单例模式的原因是在 readObject() 方法中;

               进入 readObject() —> readObject0(false) 方法 ,在 readObject0(false) 方法中找到 

               swith的 TC_OBJECT 分支,该分支下会执行 checkResolve 方法,校验 Resolve,

                这时是不是找到相似点了,我们定义的方法叫 readResolve;

         如下图所示:

    

          2、执行到核心是checkResolve 的参数 readOrdinaryObject(unshared),

                打开readOrdinaryObject 方法;

                readOrdinaryObject 方法是序列化破坏单例的主要流程;

                readOrdinaryObject 方法代码如下:

private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
             /**
                序列化破坏单例的原因:

               desc.isInstantiable() 表示一个实现了序列化接口Serializable 的类,可以在运行
                                     时被序列化,就返回true 
               desc.newInstance(): 通过反射去调用无参构造函数去创建一个新的对象
             */
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
         
         /**
             desc.hasReadResolveMethod(): 判断实现了序列化接口的类中是否包含 readResolve
                                          方法,若包含则返回true
          */
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            /**
                解决序列化对单例的破坏

                desc.invokeReadResolve(obj): 通过反射调用实现序列化接口的类中的 readResolve
                           方法;再结合我们的 Singleton类中的 readResolve方法一直返回的是
                           当前单例对象,所以readObject() 方法返回的一直是当前单例对象,没有
                           新创建对象
                
                反思:在使用 ObjectInputStream 时,我们可以通过自定义 readResolve 方法来
                      实现一些定制化功能
             */
            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);
            }
        }

        return obj;
    }

最后注意 readResolve 方法的用法,可以帮助我们实现一些定制化功能

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值