您现在的位置是:首页 > 正文

单例设计模式Java实现的八种写法以及反序列化、反射的破坏与防御

2024-04-01 07:29:26阅读 2

单例设计模式概述

单例设计模式:保证一个类仅有一个实例,并且提供一个全局访问点。

所谓类的单例设计模式,就是采取一定的措施保证在整个的软件系统中,使某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为 private ,这样,就不能用 new 操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。在类的外部一开始还无法获得类的对象,只能调用该类的某个静态方法以返回类内部创建的对象。静态方法只能访问类中的静态成员变量,所以,指向类内部创建的该类对象的变量也必须声明成静态的。

主要步骤

1)构造器私有化(防止在类的外部通过new创建该类对象)
2)在类的内部创建一个对象实例
3)对外提供一个公共的静态方法 getInstance(),返回实例对象

单例模式实现方案

1. 饿汉式(静态常量,线程安全)

饿汉式:类初始化时就创建类的对象完成实例化。

JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

class Singleton {
    //类的内部创建对象
    private final static Singleton INSTANCE = new Singleton();

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

public class SingletonTest1 {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2);
    }
}

运行结果:

true

优缺点说明

1)优点:写法简单,在类初始化时就完成实例化,避免了线程同步问题,JVM保证线程安全
2)缺点:在类初始化时就完成实例化,没有达到延迟加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
3)这种方式基于类的初始化机制,避免了多线程的同步问题,不过,instance 在类初始化时就完成实例化,在单例模式中大多数都是调用getInstance()方法,但是导致类初始化的原因有很多种,因此不能确定有其他的方式(或者调用类的其他静态方法)导致类初始化,这时实例化 instance 就没有达到延迟加载的效果
4)结论:可用,但可能造成内存浪费

2. 饿汉式(静态代码块,线程安全)

将上面创建对象的步骤放入静态代码块中

class Singleton {
    private final static Singleton INSTANCE;

    //在静态代码块中创建对象
    static {
        INSTANCE = new Singleton();
    }

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

优缺点说明

1)这种方式和上面的方式类似,只不过将类实例化的过程放在了静态代码块中,也是在类初始化的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面方式是一样的。
2)结论:可用,但可能造成内存浪费

3. 懒汉式(线程不安全)

懒汉式:延迟加载,真正使用到对象时(调用 getInstance() 方法)才去创建。在Java程序中,有时候可能需要推迟一些高开销对象的实例化操作,并且只有在使用这些对象的时候才实例化,此时,程序员可能会采用延迟实例化。

class Singleton {
    private static Singleton instance;

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点说明

1)起到了延迟加载的效果,但是只能在单线程下使用。
2)如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,如果另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
3)结论:不要使用

4. 懒汉式(同步方法,线程安全,但性能差)

在 getInstance() 方法声明加上 synchronized 关键字,保证线程安全。同一时刻只能有一个线程进入同步方法执行。

class Singleton {
    private static Singleton instance;

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
    //使用synchronized关键字修饰getInstance()方法保证线程安全
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点说明

1)保证了线程安全
2)效率太低了,每个线程在想获得类的实例时,执行getInstance()方法都要进行同步操作,频繁的加锁、释放锁需要消耗资源,使用synchronized关键字是一种重量级的同步机制,开销庞大、效率低。而其实这个方法只执行一次实例化代码就够了,如果对象已经创建,直接返回已创建的对象就行了,无需执行同步操作
3)结论:在实际开发中,不推荐使用这种方式

5. 懒汉式(同步代码块,但并不能保证线程安全)

同一时刻只能有一个线程进入同步代码块执行

class Singleton {
    private static Singleton instance;

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
    public static Singleton getInstance() {
        if (instance == null) {
            //可能有多个线程通过if判断
            synchronized(Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

优缺点说明

1)这种方式,本意是想对第四种实现方式改进,因为同步方法效率太低, 改为同步产生实例化的代码块
2)但是这种同步并不能保证线程安全,跟第3种实现方式遇到的情形一 致,假如一个线程进入了 if (singleton == null) 判断语句块, 还未来得及往下执行,另一个线程也通过了这条判断语句,同步代码块就会被多次执行,这时便会产生多实例
3)结论:不能使用

6. 懒汉式(双重检查加锁,线程安全)

getInstance()方法进行两次if (singleton == null)判断,同一时刻只能有一个线程进入同步代码块执行,在同步代码块中再加一层if (singleton == null)判断

class Singleton {
    private static volatile Singleton instance;

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

单例的引用需要用volatile关键字修饰,volatile关键字的作用:

禁止指令重排序

实际上创建对象要经过如下3个步骤:

1)分配对象所需的内存空间
2)初始化对象
3)设置 instance 变量指向刚分配的内存地址
在这里插入图片描述
2)和 3)的顺序可能会被重排序,在单线程下,访问对象是不受影响的;但在多线程下,可能会发生问题,访问一个没有初始化完成的对象操作其内部数据时,可能会抛出空指针异常。使用volatile关键字修饰可以禁止重排序,防止第3步先于第2步执行。
在这里插入图片描述
保证内存可见性

由于可见性问题,线程0创建了实例,且变量instance还未同步到主存中;此时线程1从主存中拿到的instance变量还是null
如果加上了volatile修饰instance之后,保证了内存可见性,一旦线程0返回了实例,线程1可以立即发现instance不为null(主要用在第一层if判断,同步代码块在第二层if判断就可以保证内存可见性)。

优缺点说明

1)进行了两次if (singleton == null)检查,可以保证线程安全
2)实例化代码只执行一次,后面再次访问时,判断if (singleton == null),直接返回已实例化对象,也避免反复进行方法同步
3)线程安全,延迟加载,效率较高
4)结论:推荐使用

7. 静态内部类(线程安全)

class Singleton {
    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //静态内部类定义静态属性,调用构造器,基于类初始化
    private static class SingletonInnerClass {
        private final static Singleton INSTANCE = new Singleton();
    }

    //对外提供一个公共的静态方法 getInstance(),返回静态内部类中创建的单例对象
    public static Singleton getInstance() {
        return SingletonInnerClass.INSTANCE;
    }
}

优缺点说明

1)一个类只会被类加载器加载一次,JVM保证线程安全

2)装载外部类Singleton并不会导致内部类SingletonInnerClass的装载,调用getInstance方法访问静态属性时会装载内部类,从而完成Singleton的实例化
3)优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
4)结论:推荐使用

8. 枚举(线程安全)

利用枚举的实例是有限个数的特性实现单例模式,如果我们只定义一个实例就相当于是单例了。

enum  Singleton {
    //单例对象
    INSTANCE;

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
	//其他方法
    public void method() {
        System.out.println("方法执行了...");
    }
}

优缺点说明

1)借助JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止通过反射、反序列化重新创建新的对象。
2)结论:推荐使用

反序列化破坏单例模式及解决方案

反序列化破坏饿汉式单例,让单例类实现Serializable接口

package demo1;

import java.io.*;

class Singleton implements Serializable {
    //类的内部创建对象
    private final static Singleton INSTANCE = new Singleton();

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

public class SingletonTest1 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //获取单例对象
        Singleton instance = Singleton.getInstance();
        //创建序列化流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        //将单例对象写入文件中
        oos.writeObject(instance);
        //创建反序列化流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        //从文件中读取对象
        Singleton newInstance = (Singleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

运行结果:

demo1.Singleton@14ae5a5
demo1.Singleton@6d03e736
false

解决方案

在单例类Singleton中添加一个readResolve()方法:

private Object readResolve() {
    return INSTANCE;
}

调用readObject()方法会查找单例类一个方法名为 readResolve 的方法,若存在方法名为 readResolve 的方法,则执行该方法,在该方法中返回单例类中创建的单例对象,从而保证从单例类中获取的对象和通过反序列化获取的是同一个对象。

通过测试发现前7种方案均不能防反序列化重新创建新的对象。

反序列化破坏枚举单例

package demo8;

import java.io.*;

enum  Singleton {
    INSTANCE;

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public void method() {
        System.out.println("方法执行了...");
    }
}

public class SingletonTest8 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //获取单例对象
        Singleton instance = Singleton.getInstance();
        //创建序列化流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        //将单例对象写入文件中
        oos.writeObject(instance);
        //创建反序列化流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        //从文件中读取对象
        Singleton newInstance = (Singleton) ois.readObject();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

运行结果:

INSTANCE
INSTANCE
true

调用readObject()方法若发现单例类型是枚举,则返回枚举中已存在的枚举对象,因此枚举单例能够防反序列化重新创建新的对象。

反射破坏单例模式及解决方案

前7种方案均不能防反射重新创建新的对象

在类加载初始化时就已创建好单例对象的单例模式

以饿汉式单例为例:

package demo1;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

class Singleton {
    //类的内部创建对象
    private final static Singleton INSTANCE = new Singleton();

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {}

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

public class SingletonTest1 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //正常获取单例对象
        Singleton instance = Singleton.getInstance();
        
        //获取单例类的Class对象
        Class<Singleton> clazz = Singleton.class;
        //获取单例类的私有无参构造器
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        //放开构造器对象的访问权限
        constructor.setAccessible(true);
        //反射创建单例实例
        Singleton newInstance = constructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

运行结果:

demo7.Singleton@1b6d3586
demo7.Singleton@4554617c
false

解决方案

在构造器中加上处理逻辑,只针对在类加载初始化时就已经创建好单例对象的单例模式有效,即饿汉式和基于静态内部类实现的懒加载单例模式有效,对其他懒加载实现方式无效

例如:若已存在单例对象,则直接抛出异常:

private Singleton() {
    if (INSTANCE != null) {
        throw new RuntimeException("单例构造器禁止反射调用");
    }
}

将main方法首行获取单例对象的代码放在反射创建单例对象代码的后面,上面的解决方案仍然生效,程序抛出异常。

对于不是在类加载初始化时就已创建好单例对象的单例模式是无法避免的

以双重检查加锁单例模式为例:

package demo6;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

class Singleton {

    private static volatile Singleton instance;

    //构造器私有化(防止外部通过new创建对象)
    private Singleton() {
        if (instance != null) {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

    //对外提供一个公共的静态方法 getInstance(),当需要使用到该类的对象时才去创建
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

public class SingletonTest6 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        //获取单例类的Class对象
        Class<Singleton> clazz = Singleton.class;
        //获取单例类的私有无参构造器
        Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
        //放开构造器对象的访问权限
        constructor.setAccessible(true);
        //反射创建单例实例
        Singleton newInstance = constructor.newInstance();
        Singleton newInstance2 = constructor.newInstance();

        //正常获取单例对象
        Singleton instance = Singleton.getInstance();
        System.out.println(instance);

        System.out.println(newInstance);
        System.out.println(newInstance2);
        System.out.println(instance == newInstance);
        System.out.println(newInstance2 == newInstance);
    }
}

运行结果:

demo6.Singleton@1b6d3586
demo6.Singleton@4554617c
demo6.Singleton@74a14482
false
false

反射破坏枚举单例

package demo8;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

enum  Singleton {
    INSTANCE;

    //对外提供一个公共的静态方法 getInstance(),返回实例对象
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public void method() {
        System.out.println("方法执行了...");
    }
}

public class SingletonTest8 {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<Singleton> clazz = Singleton.class;
        /*Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        for (Constructor<?> constructor : declaredConstructors) {
            System.out.println("构造方法:" + constructor); //构造方法:private demo8.Singleton(java.lang.String,int)
        }*/
        Constructor<Singleton> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        Singleton instance = declaredConstructor.newInstance("instance", 0);
        System.out.println(instance);
    }
}

反射创建单例对象失败,程序抛出异常:

Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at demo8.SingletonTest8.main(SingletonTest8.java:53)

Constructor类的newInstance方法中有这样一段代码:

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");

当单例类型是枚举时,直接抛出 IllegalArgumentException,因此枚举单例能够防反射重新创建新的对象

单例模式在JDK中的应用

例如:JDK 中,java.lang.Runtime类 就是经典的单例模式(饿汉式)

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    
    //其他方法
    ...
}

网站文章

  • CSP201909-1 小明种苹果

    CSP201909-1 小明种苹果

    import java.util.Scanner; public class Main{ public static void main (String[] args) { // System.out...

    2024-04-01 07:28:42
  • RocketMQ-高可用+扩展性

    名词约定 Region:物理区域,例如不同的机房 Broker-MS:broker最基本的高可用master-slave结构,包含1台master+1台slave Broker-Cluster:由多个...

    2024-04-01 07:28:33
  • 计算机四级理论知识试卷答案,计算机系统操作员四级理论知识试卷(含答案).doc...

    计算机系统操作员四级理论知识试卷(含答案)职业技能鉴定国家题库计算机操作员四级理论知识试卷单项选择题(第1题~第160题。选择一个正确的答案,将相应的字母填入题内的括号中。每题0.5分,共80分。)正...

    2024-04-01 07:28:25
  • 计算机管理术语路径描述的是,directory

    directory(计算机语言术语)语音编辑锁定讨论上传视频directory,英文单词。意思是n.人名地址录,(电话)号码簿;(计算机文件或程序的)目录。adj.指导的,指挥的;咨询的;管理的。di...

    2024-04-01 07:27:37
  • 【C++】c++入门

    【C++】c++入门 1.c++的诞生 2.c++和c的关系 3.c++的开发过程 4.c++的特点 C++是一种面向对象的计算机程序设计语言,由美国的本贾尼·斯特劳斯特卢普博士在20世纪80年代初期发明并实现。C++是C语言的继承,进一步扩充和完善了C语言,成为一种面向对象的程序设计语言。 它是一种静态数据类型检查的、支持多重编程范式的通用程...

    2024-04-01 07:27:30
  • 单元测试快速自定义生成---开发者的福音

    单元测试快速自定义生成---开发者的福音

    作为一个开发者,越发觉得单元测试是必须的,至于单元测试是由开发编写还是测试编写,我觉得还得看公司的技术氛围。有一个好的编写单元测试习惯的开发者,代码质量肯定是很好的,可以随时校验自己开发和改写接口的快速检查工具。也避免了测试提的bug多而影响个人绩效(有些公司把bug计入考核范围内)。而作为开发者又不想过多花费时间在单元测试中(毕竟一般开发很忙的),所以本文提供了一种快速自定义的生成...

    2024-04-01 07:27:23
  • jquery 和 js 的区别

     1、加载DOM区别 JavaScript: window.onload function first(){ alert(&#39;first&#39;); } function second(){ alert(&#39;second&#39;); } window.onload = first; window.onload = second; //只会执行第二个wind

    2024-04-01 07:26:42
  • 网络安全应届生简历怎么写?

    网络安全应届生简历怎么写?

    很多简历基本上都是从几个方面写的。1.个人基本信息 - 姓名、毕业时间、性别、毕业学校…各种个人信息,这个按照自己实际情况写就行了。2.项目经历,大学生可能项目经历很少,那你想下有没有跟着老师做过一些...

    2024-04-01 07:26:34
  • ReactiveX 简介

    ReactiveX 简介

    Observable在ReactiveX中,观察者订阅了一个Observable。然后,该观察者对Observable发出的任何项目或项目序列做出反应。这种模式有利于并发操作,因为它不需要在等待Observable发出对象时阻塞,而是以观察者的形式创建一个哨兵,随时准备在Observable所做的任何时候做出适当的反应。这个页面解释了反应模式是什么以及Observables和观察者是什么(...

    2024-04-01 07:26:26
  • Go 1.18 系列篇(四):一文掌握 Fuzzing 模糊测试

    Go 1.18 系列篇(四):一文掌握 Fuzzing 模糊测试

    系列导读:1、Go 1.18 系列篇(一):如何升级 Go 1.18 ?2、Go 1.18 系列篇(二):一文掌握泛型的使用3、Go 1.18 系列篇(三):一文掌握 Go 工作区模式1. 什么是模糊...

    2024-04-01 07:25:47