设计模式23种设计模式Java

java设计模式之单例模式

2022-07-19  本文已影响0人  亿万里南

什么是单例模式?

单例模式:是指在内存中有且只会创建一次对象的创建型-设计模式,在程序多次使用同一个对象作用相同的时候,为了防止频繁创建和消费对象,单例模式可以让程序中只创建一个对象

单例模式的优点:

在内存中只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁对象,避免对资源的占用和浪费

单例模式的缺点:

1、由于单例模式中没有抽象层,因此单例类的扩展有很大的困难

2、单例类职责过重,在一定程度违背了 “单一职责原则”

3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池设计为单例类,会导致共享连接池对象过多出现连接池溢出,如果实例出的对象长时间不被利用,系统会认为是垃圾进行回收,这将导致对象的状态丢失

单例模式的适用场景:

1、需要频繁实例化然后销毁的对象

2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象

3、一些开发工具类

4、一些大量使用的配置文件对象

单例模式的特点:

1、单例模式的构造方法是私有的

2、单例对象必须由单例类自行创建

3、单例类对外提供统一获取实例的静态方法

单例模式的实现;

1、饿汉式单例(线程安全)

饿汉式是最简单的单例模式写法,类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以保证了线程安全,但是如果长时间没使用这个方法,会浪费系统资源,所以不建议使用。

/**
 * @Author LvHui
 * @Date 13:28 2022/7/14
 * @Description 单例模式的实现之饿汉式
 * 优点:线程安全,调用效率高,
 * 缺点:不能延迟加载,如果不使用则会浪费系统资源
 * 测试后结论;因为是类初始化的时候加载的静态变量,所属在使用的时候不会出现线程安全的问题,又因为获取实例方法没有同步锁所以效率会高
 **/
public class HungerType {

    //类初始化时,立即加载这个对象
    private static final HungerType hungerType = new HungerType();

    //私有化构造器
    private HungerType() {
    }

    //提供统一的访问方法
    public static HungerType getInstance() {
        return hungerType;
    }
}
2、懒汉式单例(线程不安全)

该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:

/**
懒汉式单例-线程不安全
**/
public class LazyType {

    private static LazyType lazyType = null;

    private LazyType() {
    }

    public static LazyType getInstance() {
      
        if (lazyType == null) {

            lazyType = new LazyType();

        }
        return lazyType;
    }
}


这种方式实现的单例模式有一个问题,如果多个线程同时调用getInstance()方法时,由于没有锁机制,会导致实例化两个实例的情况,因此在多线程环境下是不安全

3、懒汉式单例(线程安全)
/**
懒汉式单例-线程安全
**/
public class LazyType {

    private static LazyType lazyType = null;

    private LazyType() {
    }
    //通过增加同步锁来解决线程安全问题
    public synchronized   static LazyType getInstance() {
      
        if (lazyType == null) {

            lazyType = new LazyType();

        }
        return lazyType;
    }
}

如上代码所示,getInstance()方法添加了同步锁,虽然解决了线程安全问题,但却也带来了另外一个问题,就是每次获取实例都需要加锁和释放锁,效率较低,继续往下优化代码

4、懒汉式单例(双重检测DCL)

经过思考我们判断只有第一次获取实例的时候才会有线程安全问题,所以我们考虑使用懒加载方式,只需要第一次的时候进行加锁,后续的操作直接返回实例,但如果只有一层判断后再加锁会有问题,如第一次两个线程同时满足第一个判空条件,然后等待获取锁,破坏了单例唯一性条件,所以需要进行二次校验判空语句。


/**
懒汉式单例-双重检测(DCL即 double-checked locking)
**/
public class LazyType {
    //增加volatile关键字
    private static volatile  LazyType lazyType = null;

    private LazyType() {
    }
   //采用双层检测方式获取实例对象
    public static LazyType getInstance() {      
        if (lazyType == null) {
          synchronized (LazyType .class){
            if (lazyType == null) {
            lazyType = new LazyType();
          }
          }
       }
        return lazyType;
    }
}
但这样就没问题了吗?

由于java内存模型允许 “无序写入”,有些时候编译器会因为性能问题,把代码进行指令重排序,可能顺序发生颠倒一般而言初始化操作并不是一个原子操作,而是分为三步

1、在堆内存中开辟对象所需要的空间,分配地址
2、根据类加载器的顺序进行初始化对象执行构造方法
3、把堆分配的地址返回给栈中的引用变量

在成员变量lazyType上修饰了volatile关键字,该关键字是为了保证可见性,防止指令重排带来的顺序性问题,因为初始化对象。不是一个原子操作,jvm可能为了优化代码会进行指令重排,导致执行了顺序发生改变,如执行流程原本是1->2->3,但可能由于指令重排改为1->3->2,这就会导致如果执行到第二步操作,另一个线程获取实例,判断不为空,获取到尚未构造完成的对象,所以为了避免这个问题,则需要用到volatile关键字来防止指令重排保证操作的有序性。

5、静态内部类单例(线程安全,懒加载)

静态内部类单例模式也比较推荐的一种单例实现,因为相比懒汉式,它用更少的代码量达到了延迟加载的目的,这种方式不仅能够保证线程安全,也能保证单例对象的唯一性,同时也延迟实例化,是一种非常推荐的方式。

那么静态内部类是如何实现线程安全呢?首先,我们先了解一下类的加载时机

java虚拟机在仅有5种情况下会对类进行初始化
1、遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时读取、或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
2、使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化
3、初始化一个类的时候发现有父类还未进行初始化,则先初始化父类
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。

当getInstance()方法被调用时,静态内部类才在StatisticsInnerClassType 的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

此处摘自 :深入理解单例模式

/**
 * @Author LvHui
 * @Date 23:37 2022/7/16
 * @Description 单例模式-静态内部类方式 懒加载
 * 静态内部类能实现单例模式有以下两点
 * 1、jvm在加载类的时候不会加载静态内部类,在使用到静态内部类的时候才会对静态内部类进行加载,和懒汉模式一样,节省系统资源
 * 2、jvm底层保证类加载的安全,即使在高并发情况下,类的加载只有一次,就保证了创建单例时并发安全性
 **/
public class StatisticsInnerClassType {

    private StatisticsInnerClassType() {
    }

    public static StatisticsInnerClassType getInstance() {
        return InnerClass.HOLDER;
    }
    /**
     * 静态内部类与外部类的实例没有绑定关系,而且只有被调用时才会
     * 加载,从而实现了延迟加载
     */
    public static class InnerClass {
        /**
         * 静态初始化器,由JVM来保证线程安全
         */
        private static final StatisticsInnerClassType HOLDER = new StatisticsInnerClassType();
    }
}
6、枚举单例(线程安全,天生防止反射)

以上静态内部类的方式虽然解决了线程安全和延迟加载的问题,但是,还是存在一些如下问题
1、反射可以获取到构造函数并设置为可访问,并生成新的对象。
2、clone的深克隆会生成新的对象
3、反序列化生成新的对象

由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏,具体参考以下代码

/**
 * @Author LvHui
 * @Date 13:28 2022/7/14
 * @Description 单例模式的实现之枚举
 * 由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏
 * 通过实现Serializable接口增加readResolve方法来防止反序列化对单例的破坏
 **/
public class EnumSingle implements Serializable {

    private static final long serialVersionUID = 1L;

    private EnumSingle() {
    }

    public enum SingletonEnum {
        SINGLETON;
        private EnumSingle enumSingle = null;

        SingletonEnum() {
            enumSingle = new EnumSingle();
        }

        public EnumSingle getInstance() {
            return enumSingle;
        }

    }

    //此方法来防止反序列化对单例的破坏,具体可参考ObjectInputStream的readObject()方法
    private Object readResolve() {
        return SingletonEnum.SINGLETON.getInstance();
    }
}
为什么枚举类型是线程安全的?

通过反编译枚举的class文件发现属性被static final修饰,根据类加载机制,static类型的属性和方法会在类加载的过程中初始化,当第一次调用初始化类,因为初始化加载类的时候classload方法是加锁保证线程安全的,所以enum是线程安全的。

微信截图_20220719225529.png
为什么枚举类型反序列化也不会创建新的实例?

枚举类型在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找values()数组种的枚举对象同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,而普通的类反序列化是通过反射重新创建对象,破坏了单例的唯一原则。所以枚举类型反序列化也不会创建新的实例

单例的模式每种各有优缺点,但推荐的还是静态内部类和枚举的单例模式。

本人水平有限,如果有地方存在一些错误和不足,麻烦各位提醒,这边我会及时修改错误的地方避免影响大家的理解。

上一篇下一篇

猜你喜欢

热点阅读