金三银四面试题(二十):单例模式知多少?

设计模式也是面试中的热门考题,基本这个部分都是问问你知不知道XXX设计模式,有什么用,优缺点,然后再现场手写一个demo。很多时候是和spring一起考的,问问你知不知道spring框架用了哪些设计模式。今天我们来先看看单例模式。

什么是单例模式

单例模式是一种设计模式,用于确保类在应用程序中只有一个实例,并提供一个全局访问点来访问该实例。单例模式通常用于那些需要全局状态或共享资源的情况,以确保整个应用程序中只有一个实例存在,从而避免不必要的资源消耗和冲突。例子,一个应用的日志记录器(Logger)。全局一个日志器记录即可,不需要多个。

单例模式的特点包括:

  1. 私有构造函数:单例类的构造函数被设为私有,以防止外部直接创建对象实例。

  2. 静态方法或静态变量:提供一个静态方法或静态变量来访问该类的唯一实例。

  3. 延迟实例化:有时单例对象不会在应用程序启动时立即创建,而是在第一次被请求时才进行实例化。

  4. 线程安全性:在多线程环境中,需要考虑单例对象的线程安全性,确保在并发情况下也能正确地返回唯一实例。

使用单例模式的优点包括:

  • 节省资源:由于只有一个实例存在,可以避免创建多个对象所带来的资源浪费。
  • 提供全局访问点:可以通过单例对象的全局访问点方便地获取到该实例,使得全局状态或共享资源的管理更加简单。
  • 确保一致性:由于只有一个实例存在,可以确保整个应用程序中对该实例的状态保持一致。

然而,使用单例模式也可能带来一些缺点,如增加了代码的耦合性、对单例对象的依赖性过强等。因此,在使用单例模式时需要权衡利弊,并根据实际情况慎重考虑。

手写单例

可能这会需要你手写一个单例模式,单例模式有很多种写法,懒汉模式,饿汉模式,双重检查模式等。

懒汉模式

懒汉模式的懒就在于就是用的时候再去创建对象,否则什么都不做

public class LazySingleton {
    // 私有静态变量,用于保存唯一的实例
    private static LazySingleton instance;

    // 私有构造函数,防止外部直接创建对象实例
    private LazySingleton() {
        // 初始化操作
    }

    // 公共静态方法,用于获取唯一的实例
    public static LazySingleton getInstance() {
        // 延迟实例化,只有在第一次调用时才创建实例
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    // 其他方法
    public void doSomething() {
        // 执行其他操作
    }
}

懒汉式单例模式的写法由于new和赋值操作的非原子性所以该写法非线程安全.

饿汉模式

饿汉模式就是提前就已经加载好的静态static 对象

public class EagerSingleton {
    // 私有静态变量,用于保存唯一的实例,并在类加载时就进行初始化
    private static final EagerSingleton instance = new EagerSingleton();

    // 私有构造函数,防止外部直接创建对象实例
    private EagerSingleton() {
        // 初始化操作
    }

    // 公共静态方法,用于获取唯一的实例
    public static EagerSingleton getInstance() {
        return instance;
    }

    // 其他方法
    public void doSomething() {
        // 执行其他操作
    }
}

饿汉式单例模式的写法:线程安全,但饿汉模式的主要缺点是如果该单例对象在应用程序中没有被使用到,那么可能会造成资源的浪费。因为在类加载时就创建了实例,即使在后续没有被使用到,该实例也会一直存在于内存中。

双重检查模式

双重检查模式就是两次检查避免多线程造成创建了多个对象。也是一种在懒汉模式的基础上改进的线程安全的单例模式。它通过双重检查锁定来确保在多线程环境下只创建一个实例。

public class DoubleCheckedSingleton {
    // 使用 volatile 关键字确保 instance 变量的可见性
    private static volatile DoubleCheckedSingleton instance;

    // 私有构造函数,防止外部直接创建对象实例
    private DoubleCheckedSingleton() {
        // 初始化操作
    }

    // 公共静态方法,用于获取唯一的实例
    public static DoubleCheckedSingleton getInstance() {
        // 双重检查锁定,确保在多线程环境下只有一个实例被创建
        if (instance == null) {
            synchronized (DoubleCheckedSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedSingleton();
                }
            }
        }
        return instance;
    }

    // 其他方法
    public void doSomething() {
        // 执行其他操作
    }
}

这里面试官可能问你,可不可以去掉这个volatile关键字,答案是不可以,volatile 关键字的作用是确保变量的可见性和禁止指令重排序,否则可能会出现线程安全问题。
所以,双检锁单例模式的写法:线程安全。

这就结束了吗?

等等,加了volatile的双重检查看似没问题,难道这就一定可靠吗?使用 Java 的反射机制可以破坏传统的单例模式实现。通过反射,可以访问类的私有构造函数,并强制创建多个对象实例,从而违反了单例模式的原则。

import java.lang.reflect.Constructor;

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
        // 私有构造函数
    }

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

    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = null;

        try {
            // 使用反射获取私有构造函数
            Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
            // 设置可访问私有构造函数
            constructor.setAccessible(true);
            // 强制创建多个实例
            singleton2 = constructor.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("singleton1: " + singleton1.hashCode());
        System.out.println("singleton2: " + singleton2.hashCode());
        System.out.println("Are they the same instance? " + (singleton1 == singleton2));
    }
}

那要怎么办? 《Effective Java》中曾经提到过,枚举单例是一种线程安全且简洁的单例模式实现方式,它基于枚举类型的特性,在Java中保证了单例实例的唯一性。枚举类型的每个枚举常量都是单例对象,且在枚举类型被加载时就被初始化。

public enum EnumSingleton {
    INSTANCE; // 唯一的枚举常量

    // 可以添加其他成员变量和方法
    private int data;

    public int getData() {
        return data;
    }

    public void setData(int data) {
        this.data = data;
    }

    // 可以在枚举类中添加构造函数,但必须是私有的
    private EnumSingleton() {
        this.data = 0;
    }
}

在上面的示例中,EnumSingleton 是一个枚举类型,其中只有一个枚举常量 INSTANCE。由于枚举类型的特性,在类加载时,INSTANCE 常量就会被初始化为单例对象,因此无需担心多线程下的并发问题。

通过调用 EnumSingleton.INSTANCE 就可以获取到该单例对象,例如:

EnumSingleton singleton = EnumSingleton.INSTANCE;

这样就可以确保在整个应用程序中只存在一个 EnumSingleton 实例。

枚举单例的优点包括:

  • 线程安全:枚举类的实例在类加载时就被创建,保证了线程安全性。
  • 简洁:使用枚举类型实现单例模式非常简洁,不需要手动编写单例模式的代码。

因此,如果在Java中实现单例模式,推荐使用枚举类型来实现。

总结

以上就是单例模式的全部内容了,希望能帮助到大家。

在这里插入图片描述

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-21 17:26:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-21 17:26:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-21 17:26:03       82 阅读
  4. Python语言-面向对象

    2024-04-21 17:26:03       91 阅读

热门阅读

  1. ChatPDF代码解读2

    2024-04-21 17:26:03       27 阅读
  2. PyTorch 与深度学习:入门指南

    2024-04-21 17:26:03       37 阅读
  3. 支付宝支付前端如何显示

    2024-04-21 17:26:03       30 阅读
  4. 代码随想录 回溯—电话号码的字母组合

    2024-04-21 17:26:03       30 阅读
  5. Golang学习笔记_RabbitMQ的原理架构和使用

    2024-04-21 17:26:03       34 阅读
  6. windows驱动系列-概述

    2024-04-21 17:26:03       34 阅读
  7. 裸机、应用、驱动

    2024-04-21 17:26:03       31 阅读
  8. 解决端口是0问题,解决mysql无法看到3306端口监听

    2024-04-21 17:26:03       34 阅读
  9. 【软件测试】单元测试+集成测试+系统测试

    2024-04-21 17:26:03       31 阅读
  10. typescript 编译配置

    2024-04-21 17:26:03       37 阅读
  11. Edge浏览器:使用心得与深度探索

    2024-04-21 17:26:03       31 阅读