【Java】单例模式的五种实现方式以及为什么推荐枚举类

简介: # 【Java】单例模式的五种实现方式以及为什么推荐枚举类![image-20220514205504118](https://img-blog.csdnimg.cn/img_convert/2ca71ab97f5aeba284d8155b366615d4.png)## 1. 饿汉式构造私有 静态成员 提供方法获取```public class SingleTarget { private static SingleTarget instance = new SingleTarget(); private SingleTarget(){};

【Java】单例模式的五种实现方式以及为什么推荐枚举类

image-20220514205504118

1. 饿汉式

构造私有 静态成员 提供方法获取

public class SingleTarget {

    private static SingleTarget instance = new SingleTarget();

    private SingleTarget(){};

    public static SingleTarget getInstance()
    {
        return instance;
    }

}

测试一下

    public static void main(String[] args) {

        SingleTarget instance1 = SingleTarget.getInstance();
        SingleTarget instance2 = SingleTarget.getInstance();

        System.out.println( instance1 == instance2);

    }

由于 是静态的 不管你有没有调用方法去拿这个实例,他也会先加载到内存中

同时,还能通过反射创建出多个对象

//反射创建单例对象
        Class<SingleTarget> aClass = (Class<SingleTarget>) Class.forName("单例模式.饿汉式.SingleTarget");
        //获取构造方法对象
        Constructor<SingleTarget> constructor =  aClass.getDeclaredConstructor();
        //开启暴力反射
        constructor.setAccessible(true);
        //调用构造方法创建对象
        SingleTarget instance2 = constructor.newInstance();

        SingleTarget instance1 = SingleTarget.getInstance();

        System.out.println( instance1 == instance2);

image-20220514210438086

2. 懒汉式

懒汉式解决了饿汉式不能懒加载这么一个问题, 但也存在反射创建多个对象这么一个问题 且 是线程不安全的

public class SingleTarget {

    private static SingleTarget instance;

    private SingleTarget() {
    }

    ;

    public static SingleTarget getInstance() {
        if (null == instance) {
            instance = new SingleTarget();
        }
        return instance;
    }

}

反射在饿汉式中已经演示过了 这里就不在演示了 演示下线程安全问题

public class Test {
    public static void main(String[] args) {

        ExecutorService executorService = Executors.newFixedThreadPool(16);

        IntStream.rangeClosed(0,100).parallel().forEach(i->{
            executorService.submit(()->{
                System.out.println("i = " + i);
                SingleTarget instance1 = SingleTarget.getInstance();
                SingleTarget instance2 = SingleTarget.getInstance();
                if (instance1 != instance2){

                    System.out.println("出现线程安全问题");
                    System.out.println("instance1 = " + instance1);
                    System.out.println("instance2 = " + instance2);
                }

            });
        });


        //SingleTarget instance1 = SingleTarget.getInstance();
        //SingleTarget instance2 = SingleTarget.getInstance();
        //
        //System.out.println( instance1 == instance2);
    }

}

idea64_GFDh3MeIxs

为什么会有线程安全问题呢 ? 看我们的代码 当两个线程同时走到这一步的时候,都会进if 结果 创建了两个对象 你可能会说,那加锁啊

是的 加锁 因此有了双检锁模式

image-20220514212019291

3. 双检锁模式

懒汉式存在线程安全问题, 是的 加锁能解决一切线程安全问题 但是要考虑效率问题 锁加在什么地方合适呢?

image-20220514212425549

不管是不是第一次获取实例,都得等一下 效率非常低 怎么样分流呢 ?

image-20220514212628981

改成这样, 咋一看好像没什么问题 但是同样的这还是存在线程安全问题

image-20220514212809583

如果首次调用的时候多个线程同时走到 if 判断 而此时 instance 确实是 null, 那么都会进入 if 虽然同时只能一个线程去new 这个对象,但是后面的线程也能执行 所以这样写时有问题的 应该要改成这样

public class SingleTarget {

    private static SingleTarget instance;

    private SingleTarget() {};

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

}

那改成这样是不是也没问题了 ? 其实这里还存在一个小问题 由于

instance = new SingleTarget();

并不是一个原子操作 可能会存在指令重排 所以最好是加上 volatile 关键字

public class SingleTarget {

    private static volatile SingleTarget instance;

    private SingleTarget() {};

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

}

双检锁模式下 又是加锁又是双重检查 特别麻烦 并且也能通过反射创建多个对象

所以我们看下一种实现方法

4. 静态内部类

public class SingleTarget {
    
    private SingleTarget() {};

    public static class SingleTargetUtilClass{
        private static final SingleTarget instance = new SingleTarget();
    }

    public static SingleTarget getInstance()
    {
        return SingleTargetUtilClass.instance;
    }
}

与双检锁模式相比 更加简单,同时也不存在线程安全问题也能做到懒加载

但是也存在线程安全问题

image-20220514214823099

有没有十全十美的方式呢 有 枚举

5. 枚举类

public enum SingleTarget {

    INSTANCE;

    public void doAny()
    {
        System.out.println("做你想做");
    }
}

如果你想通过反射创建枚举类的实例

    Constructor<SingleTarget> declaredConstructor = SingleTarget.class.getDeclaredConstructor(String.class, int.class);
        declaredConstructor.setAccessible(true);
        SingleTarget singleTarget = declaredConstructor.newInstance();

会给你抛出异常

Cannot reflectively create enum objects

image-20220514222411524

`newInstance 方法 会进行判断 如果是枚举类 直接抛出异常

image-20220514222519890

或许你用的最多的是饿汉式去实现单例模式 但其实你可以尝试着用枚举类去实现一下 推荐 静态内部类和枚举类

相关文章
|
7天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
28 7
|
5天前
|
Java
【Java】Math、System、RunTime、BigDecimal类常用方法
【Java】Math、System、RunTime、BigDecimal类常用方法
|
3天前
|
安全 Java API
16 个最常用的 Java 实用程序类
【8月更文挑战第16天】
11 1
16 个最常用的 Java 实用程序类
|
6天前
|
存储 Java 数据库连接
Java类文件结构及类加载机制
该文章主要讨论了Java类文件的结构以及Java类的加载机制,并提到了双亲委派模型的相关内容。
Java类文件结构及类加载机制
|
1天前
|
Java 开发者
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案
在Java编程中,if-else与switch作为核心的条件控制语句,各有千秋。if-else基于条件分支,适用于复杂逻辑;而switch则擅长处理枚举或固定选项列表,提供简洁高效的解决方案。本文通过技术综述及示例代码,剖析两者在性能上的差异。if-else具有短路特性,但条件增多时JVM会优化提升性能;switch则利用跳转表机制,在处理大量固定选项时表现出色。通过实验对比可见,switch在重复case值处理上通常更快。尽管如此,选择时还需兼顾代码的可读性和维护性。理解这些细节有助于开发者编写出既高效又优雅的Java代码。
6 2
|
1天前
|
SQL Java Apache
实时计算 Flink版操作报错合集之使用parquet时,怎么解决报错:无法访问到java.uti.Arrays$ArrayList类的私有字段
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
4天前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
5天前
|
设计模式 人工智能 Java
Java 如何使用单例类
Java 如何使用单例类
5 1
|
6天前
|
前端开发 Java 编译器
【前端学java】java中的Object类和前端中的Object有什么区别(9)
【8月更文挑战第10天】java中的Object类和前端中的Object有什么区别
13 0
【前端学java】java中的Object类和前端中的Object有什么区别(9)
|
5天前
|
Java
【Java】内部类、枚举、泛型
【Java】内部类、枚举、泛型