【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 方***进行判断 如果是枚举类 直接抛出异常
或许你用的最多的是饿汉式去实现单例模式 但其实你可以尝试着用枚举类去实现一下 推荐 静态内部类和枚举类
#java求职##Java#