设计模式之单例模式(创建、单例破坏、防止破坏)

简介: 设计模式之单例模式(创建、单例破坏、防止破坏)

单例模式是23种设计模式之一,也是学习设计模式遇到的第一个设计模式。

意义:在实际开发中,会有很多通用类,比如获取数据源的类,假如在使用这些类的时候,每用一次就创建一个新的实例,这样就会创建很多实例,实际上这些实例做的作用都是一样的。要想避免这种无用的创建实例,这里就需要用到单例模式。

类型:创建型模式

实现方式:1.构造器私有化 2.在类内部创建好一个实例 3.提供一个方法来获取实例

使用方式:获取该类的实例时,使用提供的方法来获取

接下来从三个方面来介绍单例模式

单例模式创建

1.饿汉式

含义:饿汉式就是犹如它的名字一样,很饿,所以提前准备好食物(实例)。

/**
 * 1.饿汉式
 */
public class Singleton1 {
    //1.构造器私有化
    private Singleton1() {
    }
    //2.本类内部创建对象实例
    private final static Singleton1 singleton1 = new Singleton1();
    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton1 getInstance() {
        return singleton1;
    }
}

优点:这种写法非常的简练,并且线程安全。

缺点:假如这个类不会被使用到,但是在类加载的时候就会创建一个实例,造成了内存浪费。

2.懒汉式

含义:很懒,一开始不会去创建,等你到使用的时候再去创建。

/**
 * 2.懒汉式(线程不安全)
 */
public class Singleton2 {
    //1.构造器私有化
    private Singleton2() {
    }
    //2.本类内部创建对象实例
    private static Singleton2 singleton2;
    //3.提供一个获取实例的方法,但是在第一次使用的时候再去实例化(存在线程安全问题)
    public static Singleton2 getInstance() {
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}

但是这种存在线程安全问题,当多个线程调用 getInstance()方法时,有可能出现线程a执行到 singleton2 = new Singleton2(); 这行时,这时候失去cpu执行权,b线程开始执行getInstance() 方法,成功创建了一个实例,这时候a线程获取到cpu执行权,继续执行 singleton2 = new Singleton2(); 就会造成获取到两个不同的实例。

那么怎么规避这种情况那,答案就是加锁,但是仅仅的加锁还是不完美的

/**
 * 3.懒汉式(双重检查,线程安全)
 */
public class Singleton3 {
    //1.构造器私有化
    private Singleton3() {
    }
    //2.本类内部创建对象实例
    private static volatile Singleton3 singleton3;
    //3.提供一个获取实例的方法,但是在第一次使用的时候再去实例化
    public static Singleton3 getInstance() {
        if (singleton3 == null) {
            synchronized (Singleton3.class) {
                if (singleton3 == null) {
                    singleton3 = new Singleton3();
                }
            }
        }
        return singleton3;
    }
}

这里提供了一种双重检查的方法来使懒汉式变成线程安全的方式。

为什么是双重检查,而不是下面这种写法呢?

这是因为如果是下面这种写法,再每一次获取实例对象的时候,所有线程都会去竞争锁,导致额外的开销。实际上线程安全问题只存在第一次创建对象的时候。在synchronized外层套一层判断就可以规避对象创建完成之后,线程竞争锁的问题,提高了效率。

public static Singleton3 getInstance() {
    synchronized (Singleton3.class) {
        if (singleton3 == null) {
            singleton3 = new Singleton3();
        }
    }
    return singleton3;
}

为什么要使用volatile关键字?

这是因为 singleton3 = new Singleton3(); 这一步并不是原子操作,别看这是一行代码,在底层它被分成了三步操作

1.分配一部分空间用于创建对象

2.在分配好的空间创建对象

3.将创建好的对象指向变量,在这里变量是singleton3

cpu在操作时为了提高效率会出现指令重排的操作,也就是把这三步进行乱序。假如说步骤变成了 1、3、2。那么就会导致 当a线程执行到第2步(执行顺序是132)的时候失去了操作权,b线程开始执行(由于a线程没有执行第2步就把空间指向了变量),发现singleton3指向的空间不是null,但是实际上此空间并没有实例,那么b线程使用该对象时候就会报错。

使用了volatile关键字就会禁止指令重排。

优点:进行了懒加载

缺点:编写复杂,要考虑线程安全

3.静态内部类

/**
 * 4.静态内部类
 */
public class Singleton4 {
    //1.创建静态内部类来声名实例对象
    private static class InnerSingleton {
        private static final Singleton4 SINGLETON_4 = new Singleton4();
    }
    //2.构造器私有化
    private Singleton4() {
    }
    //3.提供一个获取实例的方法
    public static Singleton4 getInstance() {
        return InnerSingleton.SINGLETON_4;
    }
}

优点:也是懒加载

4.枚举

/**
 * 5.枚举
 */
public enum Singleton5 {
    INSTANCE;
}

优点:无法使用序列化和反射破坏单例

破坏单例模式

在知道怎么编写单例模式之后,实际上单例模式是可以进行破坏的,这里介绍三种方式:

1.反射

这里通过反射来破坏单例模式,虽然有很多方式在构造器中预防单例模式被破坏,但是道高一尺魔高一丈,总有办法进行破坏。

/**
 * 反射破坏单例模式
 */
public class ReflectDestroy {
    public static void main(String[] args) throws Exception {
        //1。获取Singleton1的无参构造方法
        Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
        //2.接触private的封装
        constructor.setAccessible(true);
        //3.通过反射和单例模式创建两个对象,进行比较
        Singleton1 instance1 = constructor.newInstance();
        Singleton1 instance2 = Singleton1.getInstance();
        //4.结果为false,说明两个对象不是一个对象,单例模式被破坏了
        System.out.println(instance1 == instance2);
    }
}

2.序列化

首先实现Serializable接口才可以进行序列化

/**
 * 1.饿汉式
 */
public class Singleton1 implements Serializable {
    //1.构造器私有化
    private Singleton1() {
    }
    //2.本类内部创建对象实例
    private final static Singleton1 singleton1 = new Singleton1();
    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton1 getInstance() {
        return singleton1;
    }
}

然后进行测试

public class SerializableDestroy {
    public static void main(String[] args) throws Exception {
        //1.获取实例
        Singleton1 instance1 = Singleton1.getInstance();
        //2.使用输出流保存该对象
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance"));
        out.writeObject(instance1);
        //3.使用输入流读取对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance"));
        Singleton1 instance2 = (Singleton1) in.readObject();
        //4.发现结果为false,单例被破坏
        System.out.println(instance1 == instance2);
    }
}

这里发现单例被破坏,但是可以预防。

3.克隆

首先要实现Cloneable接口

public class Singleton6 implements Cloneable{
    private Singleton6() {
    }
    private final static Singleton6 singleton6 = new Singleton6();
    public static Singleton6 getInstance() {
        return singleton6;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

测试

public class CloneDestroy {
    public static void main(String[] args) throws CloneNotSupportedException {
        Singleton6 instance1 = Singleton6.getInstance();
        Singleton6 instance2 = (Singleton6) instance1.clone();
        System.out.println(instance1 == instance2);
    }
}

发现单例被破坏,那么如何防止单例被破坏那,请看下一个章节。

如何防止单例模式被破坏

这里我们从破坏单例模式的两个方面进行讲解

1.反射

如果是枚举类,那么就不可以使用反射进行破坏

public class ReflectEnumDestroy {
    public static void main(String[] args) throws Exception {
        //1.获取枚举类的构造方法
        Constructor<Singleton5> constructor = Singleton5.class.getDeclaredConstructor(String.class, int.class);
        //2.解除封装
        constructor.setAccessible(true);
        //3.创建对象,此步骤会报错:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
        Singleton5 instance = constructor.newInstance("INSTANCE", 0);
    }
}

首先说一下第一步,为什么获取构造函数要两个参数。枚举类实际上就是一个类实现Enum接口,在Enum接口中可以看到该构造方法

那第三步为什么会报错那,可以点进 newInstance(); 这个方法

这里可以看到在jdk中就定义了不准枚举类进行反射,自然而然枚举类不可以反射创建对象。

2.序列化

又是如何在序列化中保证单例那?

其实在单例类中加一个方法即可

/**
 * 1.饿汉式
 */
public class Singleton1 implements Serializable {
    //1.构造器私有化
    private Singleton1() {
    }
    //2.本类内部创建对象实例
    private final static Singleton1 singleton1 = new Singleton1();
    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton1 getInstance() {
        return singleton1;
    }
    //4.新加入的方法,防止序列化破坏单例
    private Object readResolve() {
        return singleton1;
    }
}

然后进行测试

public class SerializableDestroy {
    public static void main(String[] args) throws Exception {
        //1.获取实例
        Singleton1 instance1 = Singleton1.getInstance();
        //2.使用输出流保存该对象
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance"));
        out.writeObject(instance1);
        //3.使用输入流读取对象
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance"));
        Singleton1 instance2 = (Singleton1) in.readObject();
        //4.发现结果为true,是单例的
        System.out.println(instance1 == instance2);
    }
}

为什么这个方法加入就可以保证单例那?

在ObjectInputStream类中可以找到答案

可以从 Singleton1 instance2 = (Singleton1) in.readObject(); 这一行的readObject()方法进入查看

如果单例类中有readResolve()方法,那么就会调用这个方法获取对象,保持单例。

枚举类本身就实现了序列化接口,直接进行序列化就可以

public class SerializableEnumDestroy {
    public static void main(String[] args) throws Exception {
        Singleton5 instance1 = Singleton5.INSTANCE;
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("instance"));
        out.writeObject(instance1);
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("instance"));
        Singleton5 instance2 = (Singleton5) in.readObject();
        //结果为true,是单例的
        System.out.println(instance1 == instance2);
    }
}

枚举类没有readResolve()方法但是也可以保持单例,这是因为枚举的实例会保存在一个map中,实际读取的时候会从map中读取保证单例。

可以说枚举的特性用来做单例最为合适,后期整理到框架,也会结合框架来丰富单例模式。

相关文章
|
3天前
|
设计模式 安全 Java
【设计模式系列笔记】单例模式
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点,以便全局范围内访问这个实例。单例模式的目标是限制一个类的实例化,确保在整个应用程序中只有一个实例存在,并提供对这个唯一实例的全局访问点。这对于控制对资源的访问、限制特定类的实例数量等场景非常有用。
122 5
|
3天前
|
设计模式 安全 测试技术
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
【C/C++ 设计模式 单例】单例模式的选择策略:何时使用,何时避免
67 0
|
3天前
|
设计模式 缓存 安全
【设计模式】单例模式:确保类只有一个实例
【设计模式】单例模式:确保类只有一个实例
26 0
|
3天前
|
设计模式 安全 Java
在Java中即指单例设计模式
在Java中即指单例设计模式
22 0
|
3天前
|
设计模式 PHP
php设计模式--单例模式(三)
php设计模式--单例模式(三)
13 0
|
3天前
|
设计模式 安全 Java
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
【JAVA】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
|
3天前
|
设计模式 安全 Java
设计模式之单例模式
设计模式之单例模式
|
2天前
|
设计模式 SQL 安全
Java一分钟之-设计模式:单例模式的实现
【5月更文挑战第16天】本文介绍了单例模式的四种实现方式:饿汉式(静态初始化)、懒汉式(双检锁)、静态内部类和枚举单例,以及相关问题和解决方法。关注线程安全、反射攻击、序列化、生命周期和测试性,选择合适的实现方式以确保代码质量。了解单例模式的优缺点,谨慎使用,提升设计效率。
18 3
|
3天前
|
设计模式
【设计模式】单例模式的三种实现方式
【设计模式】单例模式的三种实现方式
7 1
|
3天前
|
设计模式 安全 Java
【设计模式学习】单例模式和工厂模式
【设计模式学习】单例模式和工厂模式