设计模式(1):单例模式及其几种实现方式

单例模式

1、单例模式简述

2、特点介绍

3、单例模式的几种实现方式

3.1 懒汉模式

3.2 饿汉模式

3.3、静态内部类实现模式

3.4 前述三个方法实现存在的问题

3.5、枚举模式实现

3.6 Java JDK中单例模式的应用


1、单例模式简述

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

2、特点介绍

(1)意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

(2)主要解决:一个全局使用的类频繁地创建与销毁。

(3)何时使用:当您想控制实例数目,节省系统资源的时候。

(4)如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

(5)关键代码:构造函数是私有的。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

3、单例模式的几种实现方式

3.1 懒汉模式

特点

延时加载,只有在真正使用的时候,才开始实例化。

关键点

(1)线程安全问题;多线程下出现重复创建问题。
(2)doble check 加锁优化(防止多线程情况下重复创建)
(3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,对应volatile修饰的字段,可以防止指令重排。

代码实现


public class LazySingletonTest {
    public static void main(String[] args) {

//        LazySingleton instance = LazySingleton .getInstance();
//        LazySingleton instance1 = LazySingleton.getInstance();
//        System.out.println(instance==instance1);

        new Thread(()->{
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(instance);
        }).start();

        new Thread(()->{
            LazySingleton instance = LazySingleton.getInstance();
            System.out.println(instance);
        }).start();
    }
}

/**
 * 1、单例模式定义:
 *     保证一个类只有一个实例,并且提供一个全局访问点。
 *
 * 2、场景:
 *     重量级的对象,不需要多个实例,如线程池,数据库连接池。
 */

/**
 * 3、单例模式的经典实现:
 * 3.1、懒汉模式:延时加载,只有在真正使用的时候,才开始实例化。
 * (1)线程安全问题;多线程下出现重复创建问题。
 * (2)doble check 加锁优化(防止多线程情况下重复创建)
 * (3)编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加volatile关键字进行修饰,
 *     对应volatile修饰的字段,可以防止指令重排。
 */
class LazySingleton{
    private volatile static LazySingleton instance;

    private LazySingleton(){

    }

    public static LazySingleton getInstance(){
        if(instance==null){   //双重检测机制
            synchronized(LazySingleton.class){  //加锁保障并发环境下的线程安全
                if(instance==null){  //双重检测机制
                    instance = new LazySingleton();
                    //字节码层面
                    //JIT,CPU指令重排
                    //1.分配空间
                    //2.初始化
                    //3.引用赋值
                    //注意:由于编译器的指令重排,可能使得其不是安全的,在此将instance设置为volatile类型可防止指令重排,保证其安全性。
                }
            }

        }
        return instance;
    }
}

3.2 饿汉模式

特点

类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性;

关键点

(1)类加载的过程:
    ①加载二进制数据到内存中,生成对应的class数据结构,
    ②连接:验证,准备(给类的静态成员变量赋默认值),解析
    ③初始化:给类的静态变量赋初值。
(2)只有在真正使用对应的类是,才会触发初始化

代码实现

public class HungrySingletonTest {
    public static void main(String[] args) {
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton instance1=HungrySingleton.getInstance();
        System.out.println(instance==instance1);
    }

}

/**
 * 3.2、饿汉模式:类加载的初始化阶段就完成了实例的初始化。本质上就是借助jvm类加载机制,保证实例的唯一性;
 * (1)类加载的过程:
 *     ①加载二进制数据到内存中,生成对应的class数据结构,
 *     ②连接:验证,准备(给类的静态成员变量赋默认值),解析
 *     ③初始化:给类的静态变量赋初值。
 * (2)只有在真正使用对应的类是,才会触发初始化
 */
class HungrySingleton{
    private static HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){

    }

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

3.3、静态内部类实现模式

特点

使用静态内部类来实现。

关键点

(1)本质上是利用类的加载机制来保证线程安全;
(2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式;

代码实现

public class InnerClassSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//        InnerClassSingleton instance = InnerClassSingleton.getInstance();
//        InnerClassSingleton instance1 = InnerClassSingleton.getInstance();
//        System.out.println(instance==instance1);

        /*new Thread(()->{
            InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
            System.out.println(instance2);
        }).start();

        new Thread(()->{
            InnerClassSingleton instance2 = InnerClassSingleton.getInstance();
            System.out.println(instance2);
        }).start();*/

        //测试反射实例化存在的问题:
        Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();

        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton==instance);

    }

}

/**
 * 3.3、静态内部类模式:
 * (1)本质上是利用类的加载机制来保证线程安全;
 * (2)只有在实际使用的时候,才会触发类的初始化,所以也是懒加载的一种形式;
 */
class InnerClassSingleton{
    private static class InnerClassHolder{
        private static InnerClassSingleton instance = new InnerClassSingleton();
    }

    private InnerClassSingleton(){
        
    }

    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
}

3.4 前述三个方法实现存在的问题

前述的单例模式的三种实现方式:懒汉模式、饿汉模式、静态内部类实现模式,都会存在反射攻击的问题。

(1)由于Java反射机制的存在,反射可以绕过单例模式创建的类实例,再次创建类实例对象,破坏了单例模式。
(2)解决方法,可以在类的私有构造函数里面进行检查判断,不允许创建多个实例。(懒汉模式无法进行防护)

反射攻击示例如下:(以静态内部类实现示例)

//测试主要代码如下:
//测试反射实例化存在的问题:
        Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);

        InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();

        InnerClassSingleton instance = InnerClassSingleton.getInstance();
        System.out.println(innerClassSingleton==instance);

//测试的结果为:
//false

上次示例说明,通过反射会破坏单例模式。对于饿汉模式和静态内部类模式,我们可以在代码中添加代码判断以防止反射创建实例攻击。通过在私有构造函数中添加判断即可,以静态内部类为例:

private InnerClassSingleton(){
        if(InnerClassHolder.instance!=null){
            throw new RuntimeException("单例不允许创建多个实例");
        }
    }

然而,对于懒汉模式,由于实例时在第一次使用时才在获取实例的函数代码创建的,因为上述方法并不能防止反射攻击。

对于单例模式,我们有更好的实现模式——枚举类实现方式。

3.5、枚举模式实现

特点

(1)天然不支持反射创建对应的实例,且有字节的反序列化机制;

(2)利用类加载机制保证线程安全。

代码实现

public enum EnumSingleton{
    INSTANCE;
    public void print(){
        System.out.println(this.hashCode());
    }
}


class EnumSingletonTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
        /*EnumSingleton instance=EnumSingleton.INSTANCE;
        EnumSingleton instance1 = EnumSingleton.INSTANCE;
        System.out.println(instance==instance1);*/

        //反射测试
        /*Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        //执行报错,Enum类型不允许通过反射创建
        EnumSingleton instance=declaredConstructor.newInstance("INSTANCE",0);*/

        
    }
}

原理分析

我们打开JDK源码中java.lang.reflect包,对反射构造类进行源码分析,对于newInstance方法,如下:

public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            Class<?> caller = Reflection.getCallerClass();
            checkAccess(caller, clazz, clazz, modifiers);
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            //重点在此,反射机制不允许创建多个单例对象
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

上述源码说明,在Java中,反射机制不允许创建多个单例对象实例,因而使用枚举类创建实例具体天然的反反射攻击,可以保护单例模式对象的唯一性。

此外,对于对象实例的序列化,对于普通模式创建的单例,是需要实现java.io.serializable接口并指定版本号的(不指定的话,由JVM默认生成,但是代码一经修改就会改变版本号,就会出现对象反序列化不一致的情况)。

3.6 Java JDK中单例模式的应用

JDK中很多地方应用了单例模式,下面为几个示例,详情可以查看源码具体内容:

(1)Runtime类:典型的饿汉模式
(2)Spring中DefaultSingletonBeanRegistry提供了注册单例容器,添加单例到容器,获取单例的方法。
(3)Spring中的ReactiveAdapterRegister类使用了volatile防止指令重排,也使用了懒汉模式实现单例。
(4)Spring中的proxyFactoryBean类,使用代理模式来创建单例类。

本文代码链接:https://github.com/JianfuYang/2020-yjf-review/tree/master/src/designpatterns/singleton

声明:上述部分内容整理自网络,仅作为本人平时学习记录。

如有侵权,请联系删除!

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值