几个JAVA关键字构成的单例模式

单例模式

前言

单例模式,可以说是最基本但却是最重要的设计模式之一。

但时间一长,总会忘记单例模式是什么,有什么。。。

所以这一篇,是复习的一篇理解和总结。

时间线

2020.09.18 - 2020.09.20 完成初稿

2020.09.24 完成反射攻击相关内容

代办

  • 单例模式中的序列化安全

参考链接

Java-单例(廖雪峰)

关于一些原理讲得非常不错

结合JDK源码看设计模式——单例模式

定义

单例模式确保某一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

咬文嚼字

单例的单指的是单一的,即一个;例表示实例。

理解

实例即new;自行实例化,即在类内实例化;

向整个系统提供这个实例:整个系统表示一个方法。提供的方式是通过类名获取的方式。

要点

  • 某个类只有一个实例
  • 必须自行创建这个实例
  • 必须向整个系统提供这个实例

优势

  • 减少创建Java实例所带来的开销
  • 便于系统跟踪单个Java实例的生命周期,实例状态等;

关于结构的理解

其实单例模式关键在于private和public两个修饰符的使用,也可以说是2个private和1个public。

为了描述上面的理解。首先对单例模式结构的角色进行分析。

角色
  • 构造方法
  • 自身的静态成员变量
  • 静态工厂方法

对于以下类Singleton,有以下内容

public class Singleton{
    
    //自身的静态成员变量
    /*修饰符*/ static Singleton singleton;
    
    //构造方法
    private Singleton(){
        
    }
    
    //静态工厂方法
    /*修饰符*/ static Singleton getSingleton(){
        return singleton;
    }
}
修饰符

私有修饰符private可以实现类的方法或者变量不会被其它类使用的目的。

首先第1个私有修饰符,用于构造方法上,如上;

然后第2个私有修饰符,可以用在自身的静态成员变量上,或者静态工厂方法上。

最后的公有修饰符,则用在最后一个角色上。

还有一点——向整个系统提供这个实例的责任是谁?——其实谁都可以,也因为这样,有多种分类。

就是所谓的饿汉式和懒汉式。见“关于分类的理解

这里姑且把这个责任让自身的静态成员变量承担

综上,可以有2个分类,即:

  • 自身静态成员变量使用private
  • 静态工厂方法使用private

根据上面描述,则有以下的情况

  • 自身的静态成员变量使用私有修饰符

    自身的静态成员变量使用私有修饰符的话,

    如果要实现向整个系统提供这个实例的话,就必须让静态工厂方法使用public修饰。

public class Singleton{
    // 私有构造方法
    private Singleton(){
        
    }
    // 私有自身的静态成员变量
    private static Singleton singleton = new Singleton();
    
    // 公有静态工厂方法
    public static Singleton getInstance(){
        return singleton;
    }
}
  • 静态工厂方法使用私有修饰符
public class Singleton{
    private Singleton(){
        
    }
    
    public static Singleton singleton = new Singleton();
    
    //可有可无
    private static Singleton getInstance(){
        return singleton;
    }
}

思考:这个私有的静态工厂方法是否有存在的意义?

没有的,

因为在类外,公有的自身静态成员变量已经完成了向整个系统提供这个实例的责任

而且在类内,直接调用自身静态成员变量也可以完成调用

总结

以上是关于单例模式结构的理解,关键在于2个private和1个public的分配


关于分类的理解

初学单例模式的时候,觉得结构非常简单,只有一个类,但是分类却有很多,饿汉式懒汉式或者枚举等等,

又有线程安全和线程不安全的区别。

饿汉式与懒汉式的区别

饿和懒是两个形容词,一个表示吃,一个表示做。吃和做可以说是两个不同的动作,也可以说是相同的。

饿的时候要吃东西,联系动作“做”就有2种情况

  • 食物已经做好了,一饿就吃
  • 食物没有做好,一饿就做。

回到单例模式,单例模式的区别就在于食物是否做好,做好了就不懒,是饿汉式;没做好就是懒,是懒汉式;

懒不懒的区别在于是否在自身的静态成员变量自身实例化,例如:

// 不懒,即饿汉式
/*修饰符*/ static Singleton singleton = new Singleton();

// 懒汉式
/*修饰符*/ static Singleton singleton;

回看 关于结构的理解,就会明白,关于结构的理解 模块 所使用的就是相对简单的饿汉式。

懒汉式的实现

懒汉式的实现的关键在于不在自身的静态成员变量处实例化

另外,在结构上,懒汉式只能将公有修饰符使用在静态工厂方法上

原因:懒汉式有另一个名字lazy loading,即延迟加载

表示在调用方第一次调用getSingleto()时才初始化全局唯一实例

所以 静态工厂方法承担了向整个系统提供实例的责任

如果将责任设置在静态成员变量上,将无法实例。

public class Singleton{
    
    private Singleton{
        
    }
    
    private static Singleton singleton = null;
    
    public static Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

关于线程安全的理解

首先,线程安全问题是基于懒汉式而言的。利用最简单的懒汉式模式进行分析,如下:

public class Singleton{
    
    private Singleton{
        
    }
    
    private static Singleton singleton = null;
    
    public static Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}
分析

首先,该懒汉式是线程不安全的,只能在单线程的情况下使用。

如果在多线程下,由于线程的执行速度是不确定的,容易出现竞争条件,

一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行;

另一个线程也通过了这个判断语句,这时便会产生多个实例(违背了单例模式的初衷)。

所以在多线程环境下不可使用这种方式。

解决

如果要让懒汉式线程安全,就必须进行相应的改造。

当然有几种方法,因此,懒汉式有线程不安全与线程安全的区别。

改造的关键在于同步锁关键字synchronized的使用

synchronized修饰方法

使用synchronized修饰方法,简单说一下synchronized的作用:

一个线程访问synchronized锁定的范围时,其他试图访问的线程将被阻塞。

这里的范围指的是方法。

public class Singleton{
    
    private Singleton{
        
    }
    
    private static Singleton singleton = null;
    
    public synchronized static Singleton getSingleton(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

分析

情景分析

当有一个线程访问该方法时,其它线程都会被阻塞。直至该线程完成访问。

效率分析

虽然在静态工厂方法处添加同步锁synchronized后,懒汉式是线程安全的。

但是这个静态工厂方法只有第一次访问时,运行if()代码块里才需要进行同步,后面访问直接返回实例就可以了。

就是说在方法处添加同步锁synchronized将会影响效率。所以效率太低了。


通过以上分析,有个方案——在代码块处添加同步锁。


synchronized修饰代码块

引用上面所讲,

一个线程访问synchronized锁定的范围时,其他试图访问的线程将被阻塞。

这里的范围指的是代码块

public class Singleton{
    
    private Singleton{
        
    }
    
    private static Singleton singleton = null;
    
    public static Singleton getSingleton(){
        
        if(singleton == null){
            sysnchronized(this){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

分析

情景分析

由于sysnchronized的作用范围问题,所以同步锁只能作用于创建实例。

和最初的懒汉式结构是一样的概念的。即一个线程来不及往下执行,另一个线程已经创建了实例,导致产生多实例。

作用分析无效

双重检查

如果上面两个例子都无法实现在多线程情况下,使用懒汉模式。那么双重检查就是在多线程的环境下使用懒汉模式的解决方案。

双重检查,Double-checked Locking,线程安全且高性能

public class Singleton{
    
    private Singleton{
        
    }
    
    /*
    1重锁
    Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值
    避免指令重排
    */
    private static volatile Singleton singleton = null;
    
    public static Singleton getSingleton(){
        // 2重锁
        // 双重检查
        if(singleton == null){
            sysnchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton(); // 不是原子性操作
                }
                // 分配内存空间
                // 执行构造方法,构造对象
                // 将对象指向内存空间
                // 不使用 volative 可能 1 3 2
            }
        }
        return singleton;
    }
}

分析

其实双重检查是synchronized修饰代码块的一类,但是却能够实现线程安全。

其它方式实现单例

枚举实现单例

JDK1.5中添加的枚举来实现单例模式。因为Java保证枚举类的每个枚举都是单例。

不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

编码
public enum Singleton{
    INSTANCE;
    
    private Singleton getInstance{
        return INSTANCE;
    }
}
运行
Singleton singleton = Singleton.INSTANCE
分析

优点

系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能

使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。

缺点当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。

私有静态内部类

之前有个结论——2个private和1个public,如果在类内创建一个私有静态内部类,那么这个私有静态内部类就可以被类使用。

私有静态内部类的设计
private static class SingletonInstance{
    private static Singleton INSTANCE = new Singleton();
}
组装
public class Singleton{
    // 私有构造方法
    private Singleton{
        
    }
    // 私有静态内部类
    private static class SingletonInstance{
        private static Singleton INSTANCE = new Singleton();
    }
    
    // 公有静态工厂方法
    public static Singleton getSingleton(){
        return SingletonInstance.INSTANCE;
    }
}
分析

其实非常像饿汉式单例,微小区别在于将私有自身的静态成员变量替换成私有静态内部类。

这样做,有什么好处?

延迟加载:在Singleton被装载的时候并不会立即实例化,只有在需要的时候,即调用getSingleton()的时候,才会装载类SingletonInstance,从而完成Singleton()的实例化。

线程安全

效率高

序列化安全

单例模式中的两大类饿汉式和懒汉式都不是序列化。所以想要序列化安全,就必须做相应的修改。

安全的序列化与反序列化

    // 防止 序列化
    private Object readResolve(){
        return INSTANCE;
    }

待补充,学完序列化知识后进行补充

反射攻击

以下将按 攻击 -> 防御 -> 攻击 -> 防御这样顺序编写

攻击与防御

初始

public class Singleton {
    private Singleton(){}

    // 通过private static 变量持有唯一实例
    private static final Singleton INSTANCE = new Singleton();

    // 通过 public static 方法返回唯一实例
    public static Singleton getInstance(){
        return INSTANCE;
    }

}

攻击:原始反射攻击

        // 反射攻击
        try {
            Singleton01 singleton01 = Singleton01.getINSTANCE();
            Constructor<Singleton01> constructor = Singleton01.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Singleton01 singleton012 = constructor.newInstance();
            System.out.println(singleton01 == singleton012); // 输出结果 false // 反射攻击成功
        }catch (Exception e){
            e.printStackTrace();
        }

防御:构造器加锁

    private Singleton(){
        synchronized (Singleton01.class){
            // 防止反射攻击
            if(INSTANCE != null){
                throw new RuntimeException("防止反射攻击");
            }
        }
    }
// 输出
/*
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 zhj.pro.reflectionattack.Main.main(Main.java:22)
Caused by: java.lang.RuntimeException: 防止反射攻击
	at zhj.pro.reflectionattack.Singleton.<init>(Singleton.java:18)
	... 5 more
原始攻击失败!
*/

攻击:使用反射创建对象

        try {
            Constructor<Singleton02> constructor = Singleton02.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Singleton02 singleton022 = constructor.newInstance();
            Singleton02 singleton023 = constructor.newInstance();
            System.out.println(singleton022 == singleton023); // 输出false 攻击成功
        } catch (Exception e){
            e.printStackTrace();
        }

防御:标记位防御

    private static boolean flag = false; 

	private Singleton03(){
        if(!flag){
            flag = true;
        }
        else{
            throw new RuntimeException("请不要进行反射攻击");
        }
    }

攻击:修改标记位

        try {
            Field field = Singleton03.class.getDeclaredField("flag");
            field.setAccessible(true);
            Constructor<Singleton03> constructor = Singleton03.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);

            Singleton03 singleton03 = constructor.newInstance();
            field.set(singleton03,false);
            Singleton03 singleton031 = constructor.newInstance();

            System.out.println(singleton03 == singleton031);

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

上面一连串的攻击与防御,可以说明对于反射攻击,懒汉式并不安全。


最终方案:枚举

参考链接:为什么使用枚举实现单例(避免序列化,反射问题)

攻击与防御

原始的枚举单例

public enum Singleton {

    INSTANCE;

    private static Singleton getInstance(){
        return INSTANCE;
    }
}

第一波攻击:使用反射构造对象

        try {
            Constructor<Singleton> singletonConstructor = Singleton.class.getDeclaredConstructor();
            singletonConstructor.setAccessible(true);
            Singleton singleton2 = singletonConstructor.newInstance();
            System.out.println(singleton2 == singleton);
        } catch (Exception e){
            e.printStackTrace();
        }

// 输出
/*
java.lang.NoSuchMethodException: zhj.pro.reflect.Singleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at zhj.pro.reflect.Main.main(Main.java:21)
*/

可以看到,并不是反射攻击问题,而是找不到方法,也就是说不存在无参构造方法。

看一下Enum源码


public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {
    /**
     * The name of this enum constant, as declared in the enum declaration.
     * Most programmers should use the {@link #toString} method rather than
     * accessing this field.
     */
    private final String name;

    /**
     * The ordinal of this enumeration constant (its position
     * in the enum declaration, where the initial constant is assigned
     * an ordinal of zero).
     *
     * Most programmers will have no use for this field.  It is designed
     * for use by sophisticated enum-based data structures, such as
     * {@link java.util.EnumSet} and {@link java.util.EnumMap}.
     */
    private final int ordinal;
    /**
     * Sole constructor.  Programmers cannot invoke this constructor.
     * It is for use by code emitted by the compiler in response to
     * enum type declarations.
     *
     * @param name - The name of this enum constant, which is the identifier
     *               used to declare it.
     * @param ordinal - The ordinal of this enumeration constant (its position
     *         in the enum declaration, where the initial constant is assigned
     *         an ordinal of zero).
     */
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }
    /*more*/

分析

可以看到,构造方式含有2个参数,所以修改第一波攻击代码

        try {
            Constructor<Singleton> singletonConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
            singletonConstructor.setAccessible(true);
            Singleton singleton2 = singletonConstructor.newInstance();
            System.out.println(singleton2 == singleton);
        } catch (Exception e){
            e.printStackTrace();
        }
// 输出
/*
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at zhj.pro.reflect.Main.main(Main.java:23)
*/

分析

攻击失败,而且是因为这类是枚举类。

其实可以看看newInstance就会明白的——当反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            //反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
            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;
    }
总结

应用

身份证

题目来源:设计模式(第2版)—单例模式—刘伟

解析

每个人的身份证是只有一个的,当第一次生成时,即实例化时,身份证便会生成。

如果身份证丢失,需要重办,不会重新分配一个号码给你,而是将之前的身份证号码发你。

编码

使用双重检查实现

public class IdCard {

    private  IdCard(){

    }

    private String sno;

    public String getSno() {
        return sno;
    }

    public void setSno(String sno) {
        this.sno = sno;
    }

    private volatile  static IdCard idCard = null;

    public IdCard getIdCard(){
        if(idCard == null) {
            synchronized (this){
                if(idCard == null){
                    idCard = new IdCard();
                }
            }
        }
        return idCard;
    }
}
测试
public class Main {

    public static void main(String[] args) {
        IdCard idCard01,idCard02;
        idCard01 = IdCard.getIdCard();
        idCard02 = IdCard.getIdCard();
        System.out.println(idCard01 == idCard02);

        System.out.println(idCard01.getSno());
        System.out.println(idCard02.getSno());
        System.out.println(idCard01.getSno().equals(idCard02.getSno()));

    }
}
结果
第一次获取身份证
重复获取身份证
true
NO.123456
NO.123456
true

打印池

题目来源:设计模式(第2版)—单例模式—刘伟

解析

如果有2台打印机连接一台电脑,打印的时候,是其中一台打印即可还是两台都打印同一份文件?

显然,答案是前者。因为另一台打印机也打印的话,就会造成资源浪费了。

编码

使用静态内部类的方式实现

public class PrintPool {

    private  volatile static PrintPool printPool = null;

    private PrintPool(){

    }
    public PrintPool getInstance() throws PrintException {
        if (printPool==null){
            synchronized (PrintPool.class){
                if(printPool==null){
                    printPool = new PrintPool();
                }
            }
        }else{
            throw  new PrintException("打印机正在打印");
        }
        return printPool;
    }

}
测试
public class Main {

    public static void main(String[] args){

        PrintPool printPool01,printPool02;

        // 创建 第1个打印池
        try {
            printPool01 = PrintPool.getPrintPool();
        } catch (PrintPoolException e){
            System.out.println(e.getMessage());
        }

        // 创建 第2个打印池
        try {
            printPool02 = PrintPool.getPrintPool();
        } catch (PrintPoolException e){
            System.out.println(e.getMessage());
        }
    }
}

单例模式的影子

JDK中的单例模式
RunTime
public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
    
    /*more*/
    ...
}

RunTime类使用的是饿汉式单例模式,

其中使用单例模式的目的在于保证每一个类只有一个单例;

由于使用的是饿汉式单例,所以类加载器一加载类,就创建实例。

Spring Bean中的单例

区别

关联的环境不同

  • 单例设计模式是指在一个JVM进程中(理论上,一个运行的Java程序必定有一个自己的独立的JVM)仅有一个实例;
  • Spring单例是指一个Spring Bean容器中(ApplicationContext)中仅有一个实例

问题

在多线程的条件下,单例模式的单例是如何保证的?

多个线程调用的话,以下在进程中也只有一个实例么?

public class Singleton{
    // 私有构造方法
    private Singleton(){
        
    }
    // 私有自身的静态成员变量
    private static Singleton singleton = new Singleton();
    
    // 公有静态工厂方法
    public static Singleton getInstance(){
        return singleton;
    }
}

问题来源:https://www.liaoxuefeng.com/wiki/1252599548343744/1281319214514210 一条评论

分析

其实在使用静态内部类实现单例的时候就有描述过,也是同样的处理流程。

同一个类加载器能保证类只加载一次,且类内使用static修饰的语句只会执行一次。

所以类Singleton被加载后,自身的静态成员变量会被执行,这一次执行,就是实例化。

所以能够保证类Singleton只有一个实例。


总结

以上总结的单例模式有多少种?

首先饿汉式根据private和public的分配有2种;

其次,懒汉式有线程不安全与线程安全的问题。

线程不安全的有简单的懒汉式以及使用sysnchronized修饰代码块的懒汉式

线程安全的有使用sysnchronized修饰方法的懒汉式与使用双重检查的懒汉式。

所以懒汉式有4种。

另外,还有使用静态内部类与枚举实现单例的2种方式。

综上所述,这里的单例模式有2+4+2=8种。


几个关键字构成单例模式

上面讲了单例模式中的饿汉式主要由private和public两个修饰符决定的。

如果对于整个单例模式而言,单例模式可以由以下几个关键字决定:

  • private和public

    其中2个修饰符,已经说了,3个角色分配2个私有修饰符和1个公有修饰符,其中构造方法必须是private,

    还剩1个私有和公有修饰符分配给自身静态成员变量和静态工厂方法。

    另外,如果是静态内部类实现单例的话,则使用私有修饰符修饰静态内部类,最后使用公有修饰符修饰静态工厂方法。

  • static

    static关键字和private和public一样重要,不过,static是贯穿整个单例模式的。

    其中工厂方法getInstance()和自身成员变量都是使用static修饰的。

    目的在于同一个类加载器加载类时只能加载一次,且使用static修饰的方法或变量都只能加载一次。

    所以{修饰符} static Singleton singleton = new Singleton()new Singleton(),即实例化,只能够执行一次,从而保证单例模式的产生。

  • sysnchronized

    sysnchronized是同步锁,保证懒汉式的线程安全的。

    其中使用同步锁修饰方法和双重检查能够保证线程安全,但使用同步锁修饰方法的性能太低了。

    双重检查还可以。

    使用同步锁修饰代码块实际上也是线程不安全的。

    最后懒汉式还有一种最原始的,不能够在多线程的环境下使用,容易产生竞争条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值