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

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

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);

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);
    }

}

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

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

3. 双检锁模式

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

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

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

如果首次调用的时候多个线程同时走到 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;
    }
}

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

但是也存在线程安全问题

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

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

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

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

相关推荐
啾啾Fun12 分钟前
Java反射操作百倍性能优化
java·性能优化·反射·缓存思想
20岁30年经验的码农19 分钟前
若依微服务Openfeign接口调用超时问题
java·微服务·架构
曲莫终27 分钟前
SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)
java·spring boot·spring
ajassi200044 分钟前
开源 java android app 开发(十二)封库.aar
android·java·linux·开源
q567315231 小时前
Java使用Selenium反爬虫优化方案
java·开发语言·分布式·爬虫·selenium
kaikaile19951 小时前
解密Spring Boot:深入理解条件装配与条件注解
java·spring boot·spring
守护者1701 小时前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
bing_1581 小时前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
ergdfhgerty1 小时前
斐讯N1部署Armbian与CasaOS实现远程存储管理
java·docker
勤奋的知更鸟2 小时前
Java性能测试工具列举
java·开发语言·测试工具