深圳幻海软件技术有限公司 欢迎您!

设计模式之(4)——单例模式

2023-03-13

定义:单例模式属于创建型模式,该类负责创建自己的对象实例,并且确保只有单个对象被创建,同时该类提供了一种全局访问其唯一实例对象的方式;这个定义中有三个要点:1、单例类只能有一个实例;2、单例类必须自己创建自己的唯一实例;3、单例类必须可以给其他所有对象提供这一唯一实例;意图:保证一个类仅有一个实例,

  定义:单例模式属于创建型模式,该类负责创建自己的对象实例,并且确保只有单个对象被创建,同时该类提供了一种全局访问其唯一实例对象的方式;这个定义中有三个要点:1、单例类只能有一个实例;2、单例类必须自己创建自己的唯一实例;3、单例类必须可以给其他所有对象提供这一唯一实例;

  意图:保证一个类仅有一个实例,并提供一个访问它的全局节点;

  主要解决:一个全局使用的对象的频繁地创建和销毁;

  何时使用:想控制类的数目,节省系统资源的时候;

  如何解决:判断系统是否已经有这个实例,有则直接返回,没有则创建返回;

  关键代码:构造函数私有;

  应用实例:

  1、一个班级只有一个班主任;

  2、Windows是多进程多线程的操作系统,在操作一个文件的时候,就不可避免地出现多个线程或者多个进程同时操作一个文件的现象,所有的文件处理必须通过一个唯一的实例来进行处理;

  优点:

  1、内存中只有相关对象的一个实例,减少了内存开销,尤其是频繁地创建和销毁的实例;

  2、避免了对资源的多重占用;

  缺点:

  1、没有接口,不能继承,与单一职责冲突,一个类应该只关心内部逻辑,而不应该关系外部怎么来实例化它;

  使用场景:

  1、要求生产唯一序列号;

  2、WEB中的计数器,不用每次刷新都在数据库中更新一次,用单例先缓存起来;

  3、创建一个对象需要消耗过多的系统资源的时候,比如I/O和数据库连接等等;

  单例模式的几种常见写法:

package cn.com.pep.model.singleton.single2;
/**
 * 
 * @Title: Singleton  
 * @Description: 线程安全的懒汉式 
 * @author wwh 
 * @date 2022-8-24 15:07:07
 */
public class Singleton {
    
    /**
     * 是否是懒加载:是
     * 是否线程安全:是
     * 实现难度:容易
     * 描述:这种方式具备很好的lazy loading,能够在多线程中很好的工作,但是效率比较低,99%情况下不需要同步;
     * 优点:第一次调用才初始化,避免了内存的浪费;
     * 缺点:必须加锁synchronized才能保证单例,在静态方法上加的是类锁会影响效率;
     */
    
    private static Singleton instance;
    
    private Singleton() {
        // TODO Auto-generated constructor stub
    }
    
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        
        return instance;
    }

}

 

package cn.com.pep.model.singleton.single3;

/**
 * 
 * @Title: Singleton
 * @Description:饿汉式单例
 * @author wwh
 * @date 2022-8-24 15:45:01
 */
public class Singleton {
    /**
     * 是否是懒加载:否
     * 是否线程安全:是
     * 实现难度:容易
     * 描述:比较常用,但是容易产生垃圾对象;
     * 优点:没有加锁,执行效率会提高;
     * 缺点:类加载时候就会初始化,浪费内存;
     * 它基于classloader机制避免了多线程同步的问题,不过,instance在类装载的时候就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance,
     * 但也不确定有其他方式导致类加载,这时候就没有达到懒加载的效果;
     */
    
    private static Singleton instance = new Singleton();

    private Singleton() {
        // TODO Auto-generated constructor stub
    }

    public static Singleton getInstance() {
        return instance;
    }
package cn.com.pep.model.singleton.single4;
/**
 * 
 * @Title: Singleton
 * @Description:双重校验锁
 * @author wwh
 * @date 2022-8-24 16:02:19
 */
public class Singleton {
    
    /**
     * 是否是懒加载:是
     * 是否多线程安全:是
     * 实现难度:较复杂
     * 描述:这种方式采用锁双锁机制,安全且在多线程情况下能保持高性能;
     */
    // volatile关键字在这使用时,是利用其禁止虚拟机的指令重排特点,防止返回一个未被完整初始化的对象;
    private volatile static Singleton instance;

    private Singleton() {
        // TODO Auto-generated constructor stub
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
          //假如当前有两个线程同时执行获取实例的方法,cpu执行A线程执行到这个地方说明instance==null这个条件成立,此时发生了线程切换,当前线程释放了类锁,此时
          //cpu执行B线程,执行完之后instance!=null,然后又切换回A线程执行,如果不instance== null做判断会再将instace初始化一次,可能会引起其他未知的错误。
if (instance == null) { instance = new Singleton(); } } } return instance; } }
package cn.com.pep.model.singleton.single5;

/**
 * @Title: Singleton
 * @Description: 静态内部类
 * @author wwh
 * @date 2022-8-24 16:12:51
 */
public class Singleton {
    
    /**
     * 是否是懒加载:是
     * 是否是多线程安全:是
     * 实现难度:一般
     * 描述:这种方式能达到和双检锁方式一样的功效,实现更简单;
     * 这种方式同样利用了classloader机制来保证实例初始化的时候只有一个线程,它跟饿汉式不同的是:
     * 第三种方式,只要Sington类被加载了,那么instance实例就会被初始化,没有达到懒加载的效果,
     * 而这种方式即使Singleton类被加载了,但是SingletonHolder类没有被主动使用,只有通过显示调用
     * getInstance()方法时候,才会显示加载SingletonHolder类,从而实例化instance对象;
     */

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
        // TODO Auto-generated constructor stub
    }

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

    以上的四种写法大部分精力都在致力于保证多线程并发下的线程安全;因为构造方法都是私有的,也可以避免由反射获取对象实例(在未开启忽略权限检查的情况下);但是还没有解决由于序列化和反序列化导致获取的不是同一个实例的问题,那么怎么解决呢?

  答案是:在单例类中增加一个readResolve()方法,返回这个唯一的实例,就可以解决这个问题啦,这是因为反序列化的readObject()底层会做个判断,假如当前反序列化的目标对象有ReadResolve()方法,那么会调用目标类的这个方法返回一个实例对象。

  最后一种单例写法,通过枚举来实现单例,这种比较推荐,这种写法天然就是线程安全的,所以我们就不需要花费大量的精力来保证线程安全,同时既可以防止反序列化生成不同实例,又可以防止反射生成不同实例:

package cn.com.pep.model.singleton.single6;

import java.io.Serializable;

/**
 * 
 * @Title: Singleton
 * @Description:
 * @author wwh
 * @date 2022-8-24 16:49:19
 */
public enum Singleton implements Serializable{

    SPRING{

        @Override
        public void say(String message) {
            System.err.println(message);
        }
        
    };
    
    public abstract void say(String message);
}

 

  为什么既可以避免反射生成不同实例,又可以避免反序列化生成不同实例呢?下面我们一一道来:

  1、至于为什么通过反射不能生成实例对象呢?请看下这个枚举类反编译之后的代码,同时还包含一个匿名内部类:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
package cn.com.pep.model.singleton.single6;

import java.io.PrintStream;
import java.io.Serializable;

public abstract class Singleton extends Enum implements Serializable {

    public static final Singleton SPRING;
    private static final Singleton ENUM$VALUES[];

    private Singleton(String s, int i) {
        super(s, i);
    }

    public abstract void say(String s);

    public static Singleton[] values() {
        Singleton asingleton[];
        int i;
        Singleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new Singleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static Singleton valueOf(String s) {
        return (Singleton) Enum.valueOf(cn / com / pep / model / singleton / single6 / Singleton, s);
    }

    Singleton(String s, int i, Singleton singleton) {
        this(s, i);
    }

    static {
        SPRING = new Singleton("SPRING", 0) {

            public void say(String message) {
                System.err.println(message);
            }

        };
        ENUM$VALUES = (new Singleton[]{SPRING});
    }
}

/*    */ // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
/*    */ package cn.com.pep.model.singleton.single6;
/*    */ 
/*    */ import java.io.PrintStream;
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ class Singleton$1 extends Singleton
/*    */ {
/*    */ 
/*    */     Singleton$1(String s, int i)
/*    */     {
/* 14 */         super(s, i, null);
/*    */     }
/*    */     public void say(String message)
/*    */     {
/* 18 */         System.err.println(message);
/*    */     }
/*    */ }

  仔细分析这个枚举反编译之后的代码,枚举类的enum其实是个关键字,普通的枚举类其实都是一个继承了Enum的普通的java子类,所有的枚举类都具有这个公共的父类,列子中的枚举类也包含了两个静态属性SPRING和ENUM$VALUES,这两个静态属性的初始化是在静态块中进行的,类加载执行静态块中的代码时候会初始化给这两个静态属性赋值,而类加载的过程会调用ClassLoader类的loadClass()方法,这个方法的方法体是用synchronized修饰的,必定是线程安全的,所以我们说这种写法是线程安全的原因在这,并且这两个静态属性还是final修饰的,一旦初始化完成则不允许修改,初始化完成之后我们的SPRING = new Singleton("SPRING", 0),又因为我们这个枚举类中包含了抽象方法,根据java规范,抽象方法只能存在于接口或者抽象方法中,所以这个子类反编译之后必然是用abstract修饰的抽象类,众所周知抽象类是不能被实例化的,并且这两个类中没有定义无参构造方法,所以不能被反射实例化了。

  2、为什么能防止反序列化生成多个对象呢(当然我们说序列化的前提是类都实现了序列化接口)?

  首先要从枚举类的序列化说起,枚举类在序列化的时候其实只是将Singleton(key,value)中的key进行了序列化,而反序列化的时候也是通过这个key去对应的map中获取对应的Singleton(key,value)实例的,大致就是这么个逻辑,有兴趣可以翻翻源码看看。