0
点赞
收藏
分享

微信扫一扫

设计模式:枚举如何实现单例模式

凌得涂 04-09 20:00 阅读 2

单例模式

单例模式(Singleton Pattern)是设计模式中的一种,它确保一个类仅有一个实例,并提供一个全局访问点来访问该实例。这种设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
单例模式的应用场景十分广泛,主要涉及需要频繁使用某个对象而又不想重复创建的情况:

常见应用场景:

  • Windows的任务管理器和回收站:这两个都是只能打开一个实例的应用程序,符合单例模式的特点。
  • 网站的计数器:由于需要同步访问,使用单例模式可以确保计数的准确性。
  • 应用程序的日志应用:一般系统共用一个日志,因此采用单例模式可以方便地对日志进行管理和操作。
  • 数据库连接池:数据库连接是一种昂贵的资源,频繁地创建和关闭数据库连接会带来巨大的性能损耗。采用单例模式,可以维护一个数据库连接的实例,从而避免这种损耗。
  • 多线程的线程池:线程池需要方便地对池中的线程进行控制,使用单例模式可以确保线程池的唯一性,从而方便管理。

单例模式的优点

  1. 节省系统资源:由于单例模式限制了一个类只能有一个实例,这避免了因重复创建对象而浪费的内存和其他系统资源。特别是在处理大型对象或需要消耗大量资源的对象时,单例模式可以显著减少资源消耗。
  2. 提高系统性能:由于无需反复创建对象,单例模式减少了在对象创建和销毁过程中的系统开销。在频繁创建和销毁对象会导致性能问题的场景下,单例模式能有效提升性能。
  3. 简化管理:单例模式提供了一个全局访问点,使得开发者可以方便地访问和操作该类的唯一实例。这简化了对对象的管理和维护,降低了代码的复杂性。
  4. 保证数据一致性:在某些情况下,如配置信息、线程池、数据库连接等,需要确保在整个系统中只有一个实例存在,以保证数据的一致性。单例模式可以确保这些资源在全局范围内只有一个访问点,从而避免了数据不一致的问题。
  5. 易于扩展:虽然单例模式限制了类的实例数量,但它并不限制类的功能扩展。开发者可以在保持单例特性的同时,为类添加新的方法和属性,以满足不断变化的需求。

常见五种单例模式实现方式

1. 饿汉式

实现方式SingletonDemo.java

package demo1;

/**
 *  单例模式:饿汉式
 *  1 提供一个私有静态的SingletonDemo类型属性
 *  2 构造器私有化
 *  3 提供一个public静态的初始化方法getInstance()方法
 *
 * @author Anna.
 * @date 2024/4/5 20:34
 */
public class SingletonDemo {

    // 静态的SingletonDemo类型属性,初始化时,立即加载这个对象
    // 由于JVM类加载规则,加载class时进行初始化,保证了线程安全,但是没有延时加载的优势
    // 因此可能会造成内存浪费,因为无论是否使用到该实例,它都会在类加载时就被创建
    private static SingletonDemo instance = new SingletonDemo();

    // 构造器私有化
    private SingletonDemo() {
    }

    // 提供一个public的初始化方法 getInstance()方法
    public static SingletonDemo getInstance(){
        return instance;
    }
}

单线程测试:

package demo1;

public class SingleThreadedTestClient {
    public static void main(String[] args) {
        // 单线程模式
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();
        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
    }
}

执行结果:

在这里插入图片描述

多线程测试:

package demo1;

public class MultithreadingTestClient {
    public static void main(String[] args) {
        // 多线程模式
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo instance3 = SingletonDemo.getInstance();
                SingletonDemo instance4 = SingletonDemo.getInstance();

                if (instance3.hashCode() != instance4.hashCode()) {
                    System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-线程名称%s:%s%n",
                            Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
                }

            }, "线程" + i).start();
        }
    }
}

执行结果:

在这里插入图片描述

结论:

2. 懒汉式:

实现方式SingletonDemo.java

package demo2;

/**
 * 单例模式:懒汉式
 * 1 提供一个私有的静态SingletonDemo属性
 * 2 构造函数私有化
 * 3 提供一个public静态的初始化方法getInstance()方法
 *
 * @author Anna.
 * @date 2024/4/5 20:59
 */
public class SingletonDemo {

    // 1 提供一个私有的静态SingletonDemo属性
    // 这里在加载时,不进行初始化,调用getInstance()方法时进行初始化。以达到延时加载的目的
    private static SingletonDemo instance;

    // 2 构造函数私有化
    private SingletonDemo(){}

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance(){
        // 判断属性是否为空
        if(instance == null){
            // 延时加载,初始化对象
            instance = new SingletonDemo();
        }
        // 返回对象
        return instance;
    }
}

单线程测试:

package demo2;

public class SingleThreadedTestClient {
    public static void main(String[] args) throws InterruptedException {
        // 单线程模式
        System.out.println("========== 单线程测试-懒汉式单例模式是否线程安全 ==========");
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();
        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
    }
}

执行结果:

在这里插入图片描述

多线程测试:

package demo2;

public class MultithreadingTestClient {
    public static void main(String[] args) {
        // 多线程模式
        System.out.println("========== 多线程测试-懒汉式单例模式是否线程安全 ==========");
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonDemo instance3 = SingletonDemo.getInstance();
                SingletonDemo instance4 = SingletonDemo.getInstance();

                if (instance3.hashCode() != instance4.hashCode()) {
                    System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
                            Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
                }

            }, "线程" + i).start();
        }
    }
}

执行结果:多线程模式下,多执行几次我们会发现,有时懒汉式单例模式,拿到的并不是同一个对象

在这里插入图片描述

// 加锁
public static synchronized SingletonDemo getInstance(){
    // 判断属性是否为空
    if(instance == null){
        // 延时加载,初始化对象
        instance = new SingletonDemo();
    }
    // 返回对象
    return instance;
}

3. 双重检锁方式:

实现方式SingletonDemo.java

package demo3;

/**
 * 单例模式:双重检锁方式
 * 1 提供一个私有的静态SingletonDemo属性
 * 2 构造函数私有化
 * 3 提供一个public静态的初始化方法getInstance()方法
 *
 * @author Anna.
 * @date 2024/4/5 20:59
 */
public class SingletonDemo {

    // 1 提供一个私有的静态SingletonDemo属性
    // 这里在加载时,不进行初始化,调用getInstance()方法时进行初始化。以达到延时加载的目的
    // 使用volatile修饰instance,保证多线程环境下instance的可见性
    private volatile static SingletonDemo instance;

    // 2 构造函数私有化
    private SingletonDemo() {
    }

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance() {
        // 判断属性是否为空
        if (instance == null) {
            // 使用 synchronized 代码块 进行加锁
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    // 延时加载,初始化对象
                    instance = new SingletonDemo();
                }
            }

        }
        // 返回对象
        return instance;
    }
}

单线程测试:

package demo3;

public class SingleThreadedTestClient {
    public static void main(String[] args) throws InterruptedException {
        // 单线程模式·
        System.out.println("========== 单线程测试-双重检锁方式单例模式是否线程安全 ==========");
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();
        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
    }
}

执行结果:

在这里插入图片描述

多线程测试:

package demo3;

public class MultithreadingTestClient {
    public static void main(String[] args) {
        // 多线程模式
        System.out.println("========== 多线程测试-双重检锁方式单例模式是否线程安全 ==========");
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonDemo instance3 = SingletonDemo.getInstance();
                SingletonDemo instance4 = SingletonDemo.getInstance();

                if (instance3.hashCode() != instance4.hashCode()) {
                    System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
                            Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
                }

            }, "线程" + i).start();
        }
    }
}

执行结果:

在这里插入图片描述

4. 静态内部类式:

实现方式SingletonDemo.java

package demo4;

/**
 * 单例模式:静态内部类方式
 * 
 * 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
 * 2 构造函数私有化
 * 3 提供一个public静态的初始化方法getInstance()方法
 *
 * @author Anna.
 * @date 2024/4/5 21:31
 */
public class SingletonDemo {

    // 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
    static class SingletonDemoInstance{
        // 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
        private static final SingletonDemo instance = new SingletonDemo();
    }

    // 2 构造函数私有化
    private SingletonDemo() {
    }

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance() {
        return SingletonDemoInstance.instance;
    }
}

单线程测试:

package demo4;

public class SingleThreadedTestClient {
    public static void main(String[] args) throws InterruptedException {
        // 单线程模式·
        System.out.println("========== 单线程测试-静态内部类方式单例模式是否线程安全 ==========");
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();
        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
    }
}

执行结果:

在这里插入图片描述

多线程测试:

package demo4;

public class MultithreadingTestClient {
    public static void main(String[] args) {
        // 多线程模式
        System.out.println("========== 多线程测试-静态内部类方式单例模式是否线程安全 ==========");
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonDemo instance3 = SingletonDemo.getInstance();
                SingletonDemo instance4 = SingletonDemo.getInstance();

                if (instance3.hashCode() != instance4.hashCode()) {
                    System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
                            Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
                }

            }, "线程" + i).start();
        }
    }
}

执行结果:

在这里插入图片描述

5. 枚举式:

实现方式SingletonDemo.java

package demo5;

/**
 * 单例模式:枚举
 *
 * 1 定义一个枚举的元素
 * 2 枚举本省就是单例模式。有JVM从根本上提供保证。避免通过反射和反序列化的漏洞
 * 3 如需使用提供相应public 方法即可
 *
 * @author Anna.
 * @date 2024/4/5 21:31
 */
public enum SingletonDemo {
    /** 定义一个枚举元素,它就代表了单例的一个实例*/
    INSTANCD;

    public void test(){System.out.println("枚举单例内部方法调用了");}
}

单线程测试:

package demo5;

public class SingleThreadedTestClient {
    public static void main(String[] args) throws InterruptedException {
        // 单线程模式·
        System.out.println("========== 单线程测试枚举单例模式是否线程安全 ==========");
        SingletonDemo instance1 = SingletonDemo.INSTANCD;
        SingletonDemo instance2 = SingletonDemo.INSTANCD;
        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
        
        // 调用方式
        SingletonDemo.INSTANCD.test();
    }
}

执行结果:

在这里插入图片描述

多线程测试:

package demo5;

public class MultithreadingTestClient {
    public static void main(String[] args) {
        // 多线程模式
        System.out.println("========== 多线程测试枚举单例模式是否线程安全 ==========");
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                SingletonDemo instance3 = SingletonDemo.INSTANCD;
                SingletonDemo instance4 = SingletonDemo.INSTANCD;

                if (instance3.hashCode() != instance4.hashCode()) {
                    System.out.printf("多线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象-%s:%s%n",
                            Thread.currentThread().getName(), instance3.hashCode() == instance4.hashCode());
                }

            }, "线程" + i).start();
        }
    }
}

执行结果:

在这里插入图片描述

多线程模式下效率测试

package demo6;

import demo1.SingletonDemo;

import java.util.concurrent.CountDownLatch;

/**
 * 测试5中单例模式 多线程下的执行效率
 *
 * @author Anna.
 * @date 2024/4/5 21:53
 */
public class TestClient {
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        int threadNum = 10;
        CountDownLatch countDownLatch1 = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000000000; j++) {
                    SingletonDemo instance = SingletonDemo.getInstance();
                }
                countDownLatch1.countDown();
            }).start();
        }
        countDownLatch1.await();
        long end = System.currentTimeMillis();
        System.out.printf("饿汉式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);

        CountDownLatch countDownLatch2 = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000000000; j++) {
                    demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
                }
                countDownLatch2.countDown();
            }).start();
        }
        countDownLatch2.await();
        end = System.currentTimeMillis();
        System.out.printf("懒汉式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);

        CountDownLatch countDownLatch3 = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000000000; j++) {
                    demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
                }
                countDownLatch3.countDown();
            }).start();
        }
        countDownLatch3.await();
        end = System.currentTimeMillis();
        System.out.printf("双重检锁方式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);

        CountDownLatch countDownLatch4 = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000000000; j++) {
                    demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
                }
                countDownLatch4.countDown();
            }).start();
        }
        countDownLatch4.await();
        end = System.currentTimeMillis();
        System.out.printf("静态内部类方式-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);

        CountDownLatch countDownLatch5 = new CountDownLatch(threadNum);
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000000000; j++) {
                    demo2.SingletonDemo instance = demo2.SingletonDemo.getInstance();
                }
                countDownLatch5.countDown();
            }).start();
        }
        countDownLatch5.await();
        end = System.currentTimeMillis();
        System.out.printf("枚举单例-%s个多线程各自创建1000000000次-耗时:%s%n", threadNum, end - start);
    }
}

在这里插入图片描述

如何选用

  • 单例对象占用资源少,不需要延时加载:枚举式 好于 饿汉式
  • 单例对象暂用资源大,需要延时加载:静态内部类 好于 懒汉式

通过反射方式破坏单例模式

验证

package demo7;

import demo4.SingletonDemo;

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

/**
 * 通过反射破坏单例模式
 *
 * @author Anna.
 * @date 2024/4/5 22:07
 */
public class TestClient {

    public static void main(String[] args) throws Exception {
        // 静态内部内实现单例模式
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();

        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
        
        // 反射获取静态内部内实现单例模式
        Class<SingletonDemo> clazz = (Class<SingletonDemo>) Class.forName("demo4.SingletonDemo");
        // 获取构造器
        Constructor<SingletonDemo> declaredConstructor = clazz.getDeclaredConstructor(null);
        // 设置跳过安全检查
        declaredConstructor.setAccessible(true);
        // 初始化对象
        SingletonDemo singletonDemo1 = declaredConstructor.newInstance();
        SingletonDemo singletonDemo2 = declaredConstructor.newInstance();

        System.out.printf("反射方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", singletonDemo1.hashCode() == singletonDemo2.hashCode());
    }
}

执行结果:

在这里插入图片描述

优化方案

SingletonDemo.java

package demo7;

public class SingletonDemo {

    // 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
    static class SingletonDemoInstance{
        // 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
        private static final SingletonDemo instance = new SingletonDemo();
    }

    // 2 构造函数私有化
    private SingletonDemo() {
        // 反射通过调用SingletonDemo()进行初始化时,判断属性 SingletonDemoInstance.instance 是否为空,如果为空则直接抛出异常
        if(SingletonDemoInstance.instance != null){
            throw new RuntimeException();
        }
    }

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance() {
        return SingletonDemoInstance.instance;
    }
}

TestClient2.java

package demo7;

import java.lang.reflect.Constructor;

public class TestClient2 {

    public static void main(String[] args) throws Exception {
        // 静态内部内实现单例模式
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();

        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
        
        // 反射获取静态内部内实现单例模式
        Class<SingletonDemo> clazz = (Class<SingletonDemo>) Class.forName("demo7.SingletonDemo");
        // 获取构造器
        Constructor<SingletonDemo> declaredConstructor = clazz.getDeclaredConstructor(null);
        // 设置跳过安全检查
        declaredConstructor.setAccessible(true);
        // 初始化对象
        SingletonDemo singletonDemo1 = declaredConstructor.newInstance();
        SingletonDemo singletonDemo2 = declaredConstructor.newInstance();

        System.out.printf("反射方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", singletonDemo1.hashCode() == singletonDemo2.hashCode());
    }
}

执行结果:

在这里插入图片描述

通过反序列化方式破坏单例模式

序列化时java,必须实现Serializable接口,因此修改单例
SingletonDemo.java

package demo8;

import java.io.Serializable;

// 实现Serializable接口
public class SingletonDemo implements Serializable {

    // 序列化ID,用于版本控制  
    private static final long serialVersionUID = 1L;

    // 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
    static class SingletonDemoInstance{
        // 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
        private static final SingletonDemo instance = new SingletonDemo();
    }

    // 2 构造函数私有化
    private SingletonDemo() {
    }

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance() {
        return SingletonDemoInstance.instance;
    }
}

TestClient.java

package demo8;

import demo8.SingletonDemo;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * 通过反序列化单例模式
 *
 * @author Anna.
 * @date 2024/4/5 22:07
 */
public class TestClient {

    public static void main(String[] args) throws Exception {
        // 静态内部内实现单例模式
        SingletonDemo instance1 = SingletonDemo.getInstance();
        SingletonDemo instance2 = SingletonDemo.getInstance();

        System.out.printf("单线程模式下,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance2.hashCode());
        
        // 序列化instance1到本地文件中 序列化时必须实现序列化接口
        try(FileOutputStream fos = new FileOutputStream("D:/temp/a.txt"); ObjectOutputStream oos = new ObjectOutputStream(fos);){
            oos.writeObject(instance1);
        }

        // 反序列化获取instance1对象
        try(FileInputStream fis = new FileInputStream("D:/temp/a.txt"); ObjectInputStream ois = new ObjectInputStream(fis);){
            SingletonDemo instance3 = (SingletonDemo) ois.readObject();
            System.out.printf("反序列化方式,判断获取的两个单例模式对象,判断是否是同一个对象:%s%n", instance1.hashCode() == instance3.hashCode());
        }
    }
}

执行结果:

在这里插入图片描述

优化方案

为了避免当通过反序列化一个单例类的实例时,可能会创建该类的另一个实例,从而破坏单例模式的唯一性保证。

可以通过在单例类中实现readResolve方法来确保反序列化时返回的是单例的原始实例。

readResolve方法是java.io.Serializable接口中的一个特殊方法, 当反序列化对象时,如果对象所属的类定义了readResolve方法, JVM会调用这个方法,并返回其返回的对象,而不是新创建的对象。

因此,我们可以在单例类的readResolve方法中返回单例的原始实例,以确保反序列化不会创建新的实例。

package demo8;

import java.io.ObjectStreamException;
import java.io.Serializable;

// 实现Serializable接口
public class SingletonDemo implements Serializable {

    // 序列化ID,用于版本控制
    private static final long serialVersionUID = 1L;

    // 1 定义一个静态内部类,内部类中提供一个静态final的单例对象属性
    static class SingletonDemoInstance{
        // 静态内部类 利用 JVM类加载器 加载类时 首先初始化 类中静态常量到常量池中且一个class只会被初始化一次这一特性保证线程安全
        private static final SingletonDemo instance = new SingletonDemo();
    }

    // 2 构造函数私有化
    private SingletonDemo() {
    }

    // 3 提供一个public静态的初始化方法getInstance()方法
    public static SingletonDemo getInstance() {
        return SingletonDemoInstance.instance;
    }

    // readResolve方法是java.io.Serializable接口中的一个特殊方法,
    // 当反序列化对象时,如果对象所属的类定义了readResolve方法,
    // JVM会调用这个方法,并返回其返回的对象,而不是新创建的对象。
    // 因此,我们可以在单例类的readResolve方法中返回单例的原始实例,以确保反序列化不会创建新的实例。
    protected Object readResolve() throws ObjectStreamException {
        return getInstance();
    }
}

执行结果:

在这里插入图片描述

gitee源码

举报

相关推荐

0 条评论