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

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

image-202205142****4118

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-202205142****8086

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-202205142****9291

3. 双检锁模式

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

image-202205142****5549

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

image-202205142****8981

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

image-202205142****9583

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

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

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-202205142****1524

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

image-202205142****9890

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

#java求职##Java#
全部评论
学到了,确实有用,感谢分享
点赞 回复 分享
发布于 2022-05-15 18:30

相关推荐

点赞 评论 收藏
分享
评论
点赞
7
分享
牛客网
牛客企业服务