设计模式[Java]:四种常见单例模式简介及其实现
基本概念
单例模式属于设计模式中的创建型模式之一。
特点
使用单例模式的对象只有一个实例。
具体应用场景,比如QQ、Broswer这种单应用程序和插件、客户端,资源管理器等。
实现方法
实现方法目前主流的有:
- 饿汉式单例
- 懒汉式单例
- 双检锁单例
- 内部静态类单例
之所以分为这么多类型,主要围绕的是两个方面的问题,一是线程安全,二是内存占用。
不同类型的实现方式有利有弊,主要会围绕其实现难度,锁,懒加载,性能开销这四个方面评估,适合使用场景的就是最好的。
-
饿汉(Eager Initialization)
- 无锁 - 类加载时就创建实例,不存在多线程同时访问的情况,避免了同步问题,但会产生内存浪费 public class Singeton{ private static Singeton instance = new Singeton(); private Singeton (){} public static Singeton getInstance(){ return instance; } }
-
懒汉(Synchronized Lazy Initialization)
- 无锁,线程不安全 - 懒加载,显式调用时才会进行实例加载,避免内存浪费 public class Singeton{ private static Singeton instance; private Singeton(){} public static Singeton getInstance(){ if(instance == null) { instance = new Singeton(); return instance; }else { return instance; } } } - 有锁, 线程安全 - 懒加载 - 效率低,高并发不适用 public class Singeton{ private static Singeton instance; private Singeton(){} public static Singeton synchronized getInstance(){ if(instance == null) { instance = new Singeton(); return instance; }else { return instance; } } }
-
双检锁(Double-Checked Locking)
- 实例化检查+锁检查,线程安全,在多线程环境下避免加锁的性能开销,性能较高 - 具体性能高低取决于getInstance如何实现 public class Singeton{ /** * volatile关键字用于确保多线程环境下的可见性。 * volatile 是 Java 中的关键字,用于声明变量,确保多个线程能够正确地处理该变量。主要有两个作用: * 可见性(Visibility): 当一个线程修改了被 volatile 修饰的变量的值,变化会对其它线程立即可见。背后的实现机制锁会直接控制内存,而非缓存。 * 禁止指令重排序(Preventing Instruction Reordering): volatile 修饰的变量的读写操作不能被重排序,确保了在多线程环境下的正确执行顺序。 */ private static volatile Singeton instance; private Singeton(){} public static Singeton getInstance(){ // 第一次检查,若实例不存在则进入同步 if (instance == null) { // 第二次检查,进入锁检查。实现线程安全,确保只有一个实例创建 synchronized (Singeton.class) { if(instance == null) { instance = new Singeton(); } } } } }
-
登记式 / 静态内部类(Static Inner Class)
- 使用static inner class既实现了Lazy Init 又实现了Lazy Loading,与双检锁不同的是,双检锁会在类装载的同时进行初始化; 登记式将其分离,只有显式调用getIns时才会装载Holder并实例化ins,而不是Singeton加载时就完成了实例化。 - 通过final定义常量实现单例 - 通过内部类加载实现线程安全与延迟加载 /** * 对于内部类的加载,Java虚拟机会保证在同一时刻只有一个线程对类进行初始化。通过类加载器的锁(Class Initialization Lock)实现。类加载器锁是对类进行初 * 始化的唯一入口,使得多线程环境下只有一个线程能够操作类的初始化操作。类加载是由类加载器负责的,而类加载器在加载类时会采用一种单一的全局锁,锁是全局单 * 一的,所以不存在竞争问题。 */ public class Singeton{ private static class Holder{ private static final Singeton instance = new Singeton(); } private Singeton(){} public static final Singeton getInstance(){ return Holder.instance; } }