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

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

单例模式是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中读取保证单例。

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

相关文章
|
15天前
|
设计模式 存储 前端开发
前端必须掌握的设计模式——单例模式
单例模式是一种简单的创建型设计模式,确保一个类只有一个实例,并提供一个全局访问点。适用于窗口对象、登录弹窗等场景,优点包括易于维护、访问和低消耗,但也有安全隐患、可能形成巨石对象及扩展性差等缺点。文中展示了JavaScript和TypeScript的实现方法。
|
20天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
21 2
|
29天前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
29 2
|
1月前
|
设计模式 Java 数据库连接
Java编程中的设计模式:单例模式的深度剖析
【10月更文挑战第41天】本文深入探讨了Java中广泛使用的单例设计模式,旨在通过简明扼要的语言和实际示例,帮助读者理解其核心原理和应用。文章将介绍单例模式的重要性、实现方式以及在实际应用中如何优雅地处理多线程问题。
38 4
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
26天前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
|
1月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入理解与应用
【10月更文挑战第22天】 在软件开发中,设计模式是解决特定问题的通用解决方案。本文将通过通俗易懂的语言和实例,深入探讨PHP中单例模式的概念、实现方法及其在实际开发中的应用,帮助读者更好地理解和运用这一重要的设计模式。
22 1
|
1月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
27 0
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
29 0
|
2月前
|
设计模式 安全 Java
Kotlin教程笔记(57) - 改良设计模式 - 单例模式
本教程详细讲解了Kotlin中的单例模式实现,包括饿汉式、懒汉式、双重检查锁、静态内部类及枚举类等方法,适合需要深入了解Kotlin单例模式的开发者。快速学习者可参考“简洁”系列教程。
37 0