文章目录单例模式Singleton Pattern一、饿汉模式二、懒汉模式解决懒汉式线程安全问题双重校验锁提高并发性能静态内部类JDK 1.2最佳方法枚举方式JDK 1.5方法的对比单例模式Singleton Pattern单例模式即代码中的某个类 只能有一个实例不能有多个 。单例模式有两种主要实现方式饿汉模式和懒汉模式一、饿汉模式在类加载阶段就会直接创建唯一实例步骤如下首先使用static创建一个实例并立即进行实例化经过static修饰的成员准确来说应该称为“类成员”修饰为对应的类属性或类方法一个JAVA程序中一个类对象存在一份类名.class文件被加载到JVM内存中生成的一个对象那么类成员static修饰的成员也存在一份。为了防止再重新new一个实例需要把构造方法设置成private提供能拿到唯一实例的方法classSingleton{privatestaticSingletoninstancenewSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}}publicclassDemo{publicstaticvoidmain(String[]args){//Singleton singleton new Singleton();//errorSingletonsingletonSingleton.getInstance();}}线程安全看多个线程同时调用getInstance()方法时是否会出现错误饿汉模式中的getInstance仅读取变量内容如果多个线程同时读一个变量此时线程是安全的二、懒汉模式懒汉模式不会立即初始化实例而是等到使用的时候再创建。在饿汉模式的基础上进行了修改classSingleton{privatestaticSingletoninstancenull;privateSingleton(){}publicstaticSingletongetInstance(){if(instancenull){instancenewSingleton2();}returninstance;}}publicclassDemo{publicstaticvoidmain(String[]args){Singleton2singletonSingleton.getInstance();}}懒汉模式中的getInstance()方法既包含了读操作又包含了修改操作是非原子性的可能导致实例被创建出多份两个线程同时读为null同时创建新的实例存在线程不安全问题解决懒汉式线程安全问题1synchronized加锁可以通过加锁将操作打包成原子的来保障线程安全这里的类对象作为锁对象classSingleton{privatestaticSingletoninstancenull;privateSingleton(){}publicstaticSingletongetInstance(){synchronized(Singleton.class){if(instancenull){instancenewSingleton();}}returninstance;}}线程不安全是发生在instance被初始化前未初始化时多线程调用 getInstance 可能同时涉及到读和修改一旦 instance 初始化之后仅存在读操作线程也就安全了。这样初始化后及时线程安全了每次调用 getInstance 方法都需要进行加锁从而产生锁竞争的问题。双重校验锁提高并发性能2加外层 if双重检查对应的改进方案再添加一个判定条件让instance初始化之前进行加锁初始化后就不进行加锁了。里层的 if 条件不能进行省略在两个 if 条件判断的时间差内可能存在instance的修改操作若去掉里层的 if 那么就没有将读写操作进行原子性打包。此时对象一旦初始化完成后续所有线程请求都只走最外层那个 if (instance null)根本不进入 synchronized 块提高了并发性能。classSingleton{publicstaticSingletongetInstance(){if(instancenull){// 1. 先看一眼无锁synchronized(Singleton.class){// 2. 发现是空的才去排队if(instancenull){// 3. 排到了再确认一下instancenewSingleton();}}}returninstance;// 4. 大多数情况直接返回不碰锁}}3volatile 修饰防止指令重排序如果多个线程块都去调用 getInstance 方法大量的读操作会产生编译器优化编译器和处理器为了优化性能可能会改变指令执行顺序对于下面的代码new Singleton() 可以分为三个步骤分配内存空间初始化对象将引用指向内存地址编译器和 CPU 可能为了优化将 步骤 2 和步骤 3 交换单线程下无影响此时存在线程A、B均调用 getInstance() 方法A线程刚执行完步骤 3还没初始化B线程 过来看到 instance ! null 直接返回了classSingleton{privatestaticvolatileSingletoninstancenull;//加上 volatile 禁止重排序privateSingleton2(){}publicstaticSingletongetInstance(){if(instancenull){//B线程发现instance ! null但是此时的instance还未初始化synchronized(Singleton.class){if(instancenull){instancenewSingleton();//A线程将引用指向内存地址但还未初始化对象}}}returninstance;}}静态内部类JDK 1.2加 volatile 的双重检查锁已经足够完美但代码依然略显繁琐且容易写错。静态内部类单例模式利用 JVM 的类加载机制天然保证了线程安全与懒加载是实际开发中最推荐的写法。外部类 Singleton 被加载时其内部的静态内部类 Holder 不会被立即加载。只有当调用 getInstance() 方法首次访问 Holder.INSTANCE 时才会触发 Holder 类的加载与初始化// 完美版不用锁不用 volatileJVM 保证线程安全且懒加载publicclassSingleton{privateSingleton(){}privatestaticclassHolder{staticfinalSingletonINSTANCEnewSingleton();}publicstaticSingletongetInstance(){returnHolder.INSTANCE;}}最佳方法枚举方式JDK 1.5此时的INSTANCE; 是这个枚举类的唯一一个实例经过编译器处理后生成了一个 public static final 的常量publicfinalclassSingletonextendsjava.lang.EnumSingleton{publicstaticfinalSingletonINSTANCEnewSingleton();......}枚举实例是在枚举类被加载时初始化的相当于饿汉模式对于枚举Java 在反射 API 底层做了硬性拦截。同时枚举的序列化机制是 JVM 特殊处理的不遵循普通类的反序列化逻辑当反序列化一个枚举对象时JVM 会根据流中的枚举常量名称比如 “INSTANCE”直接返回该枚举类中同名的那个 static final 常量而不是重新构造一个对象。因此该方法绝对防止多次实例化天然抵御反射攻击和序列化破坏。publicenumSingleton{INSTANCE;// 定义一个枚举元素它就代表了 Singleton 的一个实例// 可以像普通类一样添加成员变量和成员方法privateStringconfigInfodefault;publicvoiddoSomething(){System.out.println(通过枚举实现单例模式);}publicStringgetConfigInfo(){returnconfigInfo;}}// 使用方式publicclassDemo{publicstaticvoidmain(String[]args){SingletonsingletonSingleton.INSTANCE;singleton.doSomething();}}方法的对比实现方式是否线程安全是否懒加载并发性能反射攻击和序列化破坏饿汉模式安全非懒加载好无法防御懒汉模式无锁不安全懒加载好无法防御懒汉模式方法加锁安全懒加载差无法防御双重检查锁 volatile安全懒加载较好无法防御静态内部类安全懒加载优秀无法防御枚举Enum安全非懒加载好绝对防御