思想:
- 构造器私有
- 实例化的变量引用私有化
- 获取实例的方法共有
实现:
饿汉式
/** * @author SHshuo * @data 2021/8/23--9:51 * 构造器私有,声明为static静态,一上来就加载全部的资源 */ public class hungryPattern { // 构造器私有 private hungryPattern(){ } // 类加载的时候初始化对象 public final static hungryPattern hungry = new hungryPattern(); // 提供对外接口 public static hungryPattern getInstance(){ return hungry; } }
DCL(双重检测模式double checked locking)懒汉式:
为什么双重检查 ?
- 第一个 if:如果不用第一个if判断,在多线程的情况下,所有的线程都会进行抢锁,也就是串行执行;
- 第二个 if:为了防止多个线程同时满足为 null 的条件,依次进入重复创建对象。
为什么使用 volatile ?
- 防止在创建对象的时候,将一个未初始化的对象引用暴露出来,从而导致不可预料的结果;
- 参考:https://cloud.tencent.com/developer/article/1905167
代码实现:
/** * @author SHshuo * @data 2021/8/23--9:56 * 构造器私有,需要用到的资源加载 * lazyPattern lazy = new lazyPattern();创建对象分为三步: * 1.分配内存空间 * 2.执行构造函数、创建对象 * 3.将对象指向内存空间 */ public class lazyPattern { // 构造器私有 private lazyPattern(){ System.out.println(Thread.currentThread().getName()); } // 先初始化对象 private volatile static lazyPattern lazy; // 对外接口、用到了再创建、并加锁 public static lazyPattern getInstance(){ // 双重检测锁模式。DCL if (lazy == null){ // 加锁、保证高并发 synchronized (lazyPattern.class){ // 为什么还要加if,因为第一个if不保证原子性 if(lazy == null){ // 防止创建对象的时候指令重排,用volatile修饰 lazy = new lazyPattern(); } } } return lazy; } // 测试 public static void main(String[] args) { for (int i = 0; i < 20; i++) { new Thread(() -> { getInstance(); }).start(); } } }
单例模式不安全,可以被反射破坏。通过反射机制调用私有构造器:
// 通过反射机制调用私有构造器 Constructor<reflexLazyPattern> declaredConstructor = reflexLazyPattern.class.getDeclaredConstructor(); declaredConstructor.setAccessible(true); // 创建对象(调用构造函数) reflexLazyPattern reflexLazyPattern = declaredConstructor.newInstance(); System.out.println(reflexLazyPattern);
反射源码分析:jvm内部不允许反射操作枚举。枚举类是天然地可以抵御序列化对单例的破坏和反射攻击的。
源码:
@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
枚举实现单例模式,防止反射破坏,最优解。
/** * @author SHshuo * @data 2021/8/23--14:23 * enum是天生单例且线程安全的,这种线程安全是jvm层面的,因为jvm保证类加载是线程安全的。 * 其次,枚举序列化的时候用的是类名和class来找到唯一枚举对象,所以序列化也能保证单例。 * 最后反射enum的时候也有保证,不论是你反射用了无参的,还是enum(String.class,int.class), * 底层都会被阻拦,因为如果是enum类型就会抛出异常,这是jdk层面的保护。 * <p> * <p> * 单元素的枚举类型已经成为实现Singleton的最佳方法 * -- 出自 《effective java》 */ public enum EnumLazyPattern { // 创建一个枚举对象、该对象天生单例 INSTANCE; // 重写构造函数、构造函数默认是private EnumLazyPattern() { System.out.println(Thread.currentThread().getName()); } // 提供对外接口 public EnumLazyPattern getInstance() { return INSTANCE; } // 测试 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> { EnumLazyPattern.INSTANCE.getInstance(); }).start(); } } }
补充:枚举实现单例模式:
1. enum是天生单例且线程安全的,这种线程安全是jvm层面的,因为jvm保证类加载是线程安全的。
2. 枚举序列化的时候用的是类名和class来找到唯一枚举对象,所以序列化也能保证单例。
3. 反射enum的时候也有保证,不论是你反射用了无参的,还是enum(String.class,int.class),底层都会被阻拦,因为如果是enum类型就会抛出异常,这是jdk层面的保护。
4. 单元素的枚举类型已经成为实现Singleton的最佳方法
-- 出自 《effective java》
5. 参考资料:
- https://www.jianshu.com/p/5647d1f598c7
- https://www.jianshu.com/p/d35f244f3770
- https://www.jianshu.com/p/9a8ea903b940
个人总结:
- 枚举实现单例模式是最优解,操作简单,自然解决了反射和序列化带来的问题。
- 说白了,就是将实例化变量私有private volatile static Class className ; 定义成INSTANCE。构造函数私有,对外接口更加简洁,直接返回INSTANCE即可。