单例模式是比较简单的设计模式,但是涉及到的知识点还是挺多,比如并发模式下的单例、序列化反序列化情况下保证单例、反射情况下保证单例等。

单例模式最基本的组成构造函数私有化、即不能随便创建对象;对外提供一个静态方法来获取对象。

一、饿汉模式

线程安全

public class Single {
    /**
     * 类成员变量
     */
    private static Single single = new Single();
    private Single() {
        System.out.println("私有构造函数");
    }
    public static Single getInstance() {
        return single;
    }
}


二、懒汉模式

线程不安全

public class Single {
    /**
     * 类成员变量
     */
    private static Single single ;
    private Single() {
        System.out.println("私有构造函数");
    }
    public static Single getInstance() {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}
三、静态内部类(懒汉+饿汉)
1、静态内部类实现单例模式(线程安全)
public class Single implements Serializable {
    static {
        System.out.println("1、外部类加载、链接、初始化");
    }
    private Single() {
        System.out.println("4、调用私有构造函数初始化单例对象");
    }
    /**
     * 获取枚举类对象
     *
     * @return
     */
    private static class InnerSingle {
        static {
            System.out.println("3、静态内部类加载、链接、初始化");
       }
        private static final Single SINGLE = new Single();
    }
 
    /**
     * 对外发布单例对象
     *
     * @return
     */
    public static Single getInstance() {
        System.out.println("2、外部类加载、链接、初始化完成");
        return InnerSingle.SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Single single = Single.getInstance();
        Single single1 = Single.getInstance();
        System.out.println(single == single1);
    }
}
-----------------------------
1、外部类加载、链接、初始化
2、外部类加载、链接、初始化完成
3、静态内部类加载、链接、初始化
4、调用私有构造函数初始化单例对象
2、外部类加载、链接、初始化完成
true

(1)、当有getstatic、setstatic、invokestatic 等指令调用外部类则会触发外部类的加载、链接、初始化过程,但是在这过程不会触发静态内部类的加载(静态内部类、非静态内部类的加载和外部类的加载无关),这里我们调用了外部类静态方法即触发了invokestatic 指令,所以进行了外部类的加载、链接、初始化流程的执行。

(2)、当外部类初始化结束后开始执行外部类静态方法

(3)、外部类静态方法中调用了内部类的静态常量,这样内部类又触发了getstatic 指令

(4)、内部类触发getstatic指令开始执行内部类的类加载、连接、初始化流程

(5)、内部类初始化就是内部类的类构造器执行过程即<clinit> 方法执行

(6)、按照开始分析的饿汉模式加载流程知道clinit方法中有putstatic 指令即给内部类的静态变量赋值

(7)、putstatic 指令赋值的具体对象就是调用外部类的私有构造函数创建的单例对象

(8)、结束了静态内部类单例的生成流程,按照<clinit> 方法执行是线程安全的来看,静态内部类也是线程安全的。

四、懒汉模式-多线程环境加类锁
懒汉模式线程安全实现

这样这个锁竞争很激烈,性能也就更低。

public static Single getInstance() {
    synchronized (Single.class) {
        if (null == single) {
            single = new Single();
        }
        return single;
    }
}
懒汉模式-多线程双重判断

会有指令重排,对象刚申请堆内存控件,默认赋值还未初始化时,被其他线程读走了

public static Single getInstance() {
    if (null == single) {
        synchronized (Single.class) {
            if (null == single) {
                single = new Single();
            }
        }
    }
    return single;
}
懒汉模式-多线程双重判断volatile防指令重排(内存屏障),内存可见性
private static volatile single = null; 
public static Single getInstance() {
    if (null == single) {
        synchronized (Single.class) {
            if (null == single) {
                single = new Single();
            }
        }
    }
    return single;
}

七、使用序列化、反序列化破坏单例模式
1.序列化、反序列化破坏单例,在反序列化时,反射创建了新对象
public class Single implements Serializable {
    /**
     * 类成员变量
     */
    private static volatile Single single;
    private Single() {
        System.out.println("私有构造函数");
    }
    /**
     * 双重锁校验
     *
     * @return
     */
    public static Single getInstance() {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException, NoSuchMethodException, IOException, ClassNotFoundException {
        Single single = getInstance();
        // 序列化
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "singles.text");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(single);
        // 反序列化
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        Single single1 = (Single) objectInputStream.readObject();
        System.out.println(single == single1);
    }
}

其实看序列化、发序列化的过程你会发现也是调用的反射来创建对象;使用序列化破坏单利很简单,那怎么防止序列化、反序列化破坏单利模式?

2、一步步分析反序列化过程、了解是怎么破坏的单例

(1)、从输入流获取对象

(2)、readObject0获取对象

单例模式2.png

(3)、获取类序列化描述符类,并使用反射获取反序列化新对象

单例模式3.png

(4)、使用constructor 构造函数对象创建新对象

单例模式4.png

(5)、readResolve方法判断,如果序列化类没有readResolve 方法则直接返回构造函数创建的对象,如果存在则获取readResolve 方法返回值

单例模式5.png

(6)、反射调用readResolve 方法

单例模式6.png

(7)、用readResolve 方法返回值替换使用constructor 构造函数器对象创建的新对象

单例模式7.png

从上面流程看到,如果序列化类无readResolve 方法则直接返回通过constructor 构造函数类对象创建的新对象,如果readResolve 方法存在则返回该方法返回值,所以在单例类序列化时添加该方法,可以避免序列化、反序列化破坏单例!

八、懒汉单例模式-反射破坏单利

因为返利能获取类中的任何变量并且能重新设置值,所以不管是添加一个计数器还是判断变量是否为空都是无法避免反射破坏单例的;下面来看看使用计数器和判断是否为null来阻止破坏单例模式是否有效。

public class Single implements Serializable {
    private static volatile boolean flag = true;
    private static volatile Single single;
    /**
     * 类成员变量
     */
    private Single() throws BindException {
        if (!flag) {
            throw new BindException("多次创建对象");
        }
        flag = false;
    }
 
    /**
     * @return
     * @throws BindException
     */
 
    public static Single getSingle() throws BindException {
        if (null == single) {
            synchronized (Single.class) {
                if (null == single) {
                    single = new Single();
                }
            }
        }
        return single;
    }
 
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException, BindException {
 
        Single single = Single.getSingle();
        Single single1 = Single.getSingle();
        System.out.println(single == single1);
        //反射设置属性
        Class<Single> singleClass = Single.class;
        Field field = singleClass.getDeclaredField("flag");
        field.setAccessible(true);
        //设置属性,满足创建对象条件
        field.set(single, true);
        Constructor<Single> constructor = singleClass.getDeclaredConstructor(null);
        Single single2 = constructor.newInstance(null);
        System.out.println(single == single2);
    }
}

反射后会重新创建新对象.

九、枚举类解决序列化、反序列化以及反射等的相关问题
public class Single implements Serializable {
    /**
     * 类成员变量
     */
    private Single() {
        System.out.println("私有构造函数");
    }
    /**
     * 内部类
     */
    private enum InnerClass implements Serializable {
        MY_SINGLE;
        private final Single single;
        /**
         * 枚举类无惨构造函数
         */
        InnerClass() {
            System.out.println("内部类构造函数");
            single = new Single();
        }
        /**
         * 私有方法
         *
         * @return
         */
 
        private Single getSingle() {
            return single;
        }
    }
 
    /**
     * 获取枚举类对象
     *
     * @return
     */
 
    public static InnerClass getEnum() {
        return InnerClass.MY_SINGLE;
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        InnerClass innerClass = getEnum();
        Single single = innerClass.getSingle();
        String path = System.getProperty("user.dir");
        File file = new File(path + File.separator + "single.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
        objectOutputStream.writeObject(innerClass);
        FileInputStream fileInputStream = new FileInputStream(file);
        ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
        InnerClass innerClass1 = (InnerClass) objectInputStream.readObject();
        System.out.println(innerClass == innerClass1);
        Single single2 = innerClass1.getSingle();
        System.out.println(single == single2);
    }
}
-----------------------------
内部类构造函数
私有构造函数
true
true
// Singleton.java
public enum Singleton {
    INSTANCE;

    public void testMethod() {
        System.out.println("执行了单例类的方法");
    }
}

// Test.java
public class Test {
 public static void main(String[] args) {
        //演示如何使用枚举写法的单例类
        Singleton.INSTANCE.testMethod();
        System.out.println(Singleton.INSTANCE);
    }
}

****************************************
--执行了单例类的方法
--INSTANCE
枚举的写法的优点:
  • 不用考虑懒加载和线程安全的问题,代码写法简洁优雅

  • 线程安全

反编译任何一个枚举类会发现,枚举类里的各个枚举项是是通过static代码块来定义和初始化的(可以见后面3.2节反编译分析单例枚举有分析到这个),它们会在类被加载时完成初始化,而java类的加载由JVM保证线程安全,所以,创建一个Enum类型的枚举是线程安全的

  • 防止破坏单例

我们知道,序列化可以将一个单例的实例对象写到磁盘,然后再反序列化读回来,从而获得一个新的实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。

Java对枚举的序列化作了规定,在序列化时,仅将枚举对象的name属性输出到结果中,在反序列化时,就是通过java.lang.Enum的valueOf来根据名字查找对象,而不是新建一个新的对象。枚举在序列化和反序列化时,并不会调用构造方法,这就防止了反序列化导致的单例破坏的问题。

对于反射破坏单例的而言,枚举类有同样的防御措施,反射在通过newInstance创建对象时,会检查这个类是否是枚举类,如果是,会抛出异常java.lang.IllegalArgumentException: Cannot reflectively create enum objects,表示反射创建对象失败。

综上,枚举可以防止反序列化和反射破坏单例。

文章作者: 刘同学
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 刘同学的小站
后端 SpringBoot Java 设计模式
喜欢就支持一下吧