视频链接: 设计模式|狂神说
单例模式是什么?
单例模式是确保一个类在整个应用程序中只有一个实例,并提供一个全局方法访问这个实例。
单例模式分为饿汉式和懒汉式。
饿汉式单例
饿汉式顾名思义就是,程序一启动就创建这个单例bean
// 饿汉式
public class Hungry {// 静态变量在类加载时就会为其赋值private final static Hungry HUNGRY = new Hungry();private Hungry() {}public static Hungry getInstance() {return HUNGRY;}
}
简单介绍一下实例化类机制:
类加载过程:
首先是new对象、访问静态成员或继承时触发类加载,类加载是通过双亲委派机制来加载的,首先是类加载器查找类的字节码文件,读入内存(加载),进行链接操作(验证检验字节码的合法性、准备为静态变量赋值、解析将符号引用转直接引用),执行类构造器方法为静态变量赋值。
实例化阶段:
为对象分配内存,将实例变量初始化为零值,设置对象头信息,调用构造方法为成员变量赋值,返回对象地址
懒汉式单例
懒汉式单例是在要用的时候再去创建单例bean。
初步代码:
// 懒汉式单例
public class Lazy {private static Lazy lazy;private Lazy() {System.out.println(Thread.currentThread().getName() + ":实例化");}public static Lazy getInstance() {if (lazy == null) {lazy = new Lazy();}return lazy;}public static void main(String[] args) {for (int i = 0; i < 5; i ++) {new Thread(()->{Lazy instance = Lazy.getInstance();System.out.println(Thread.currentThread().getName() + ": " + instance);}, "t" + i).start();}}
}
Lazy.getInstance
会触发对象实例化,第一次对象实例化时,会由类加载器加载这个类到内存中,然后到实例化阶段,会对类进行初始化,然后调用构造方法,当没有时,默认调用无参构造方法,那对于上述代码,按照预想的单例模式,应该对象只会实例化一次,也就是说,只会执行一次无参构造方法,我们来测试一下:
结果显示:
t0:实例化
t2:实例化
t4:实例化
t2: com.aof.singleton.Lazy@59a30351
t3: com.aof.singleton.Lazy@31d0f5c9
t0: com.aof.singleton.Lazy@31d0f5c9
t1: com.aof.singleton.Lazy@57f64e55
t4: com.aof.singleton.Lazy@57f64e55
很明显,对象实例化了多次,并且返回的对象也不是同一个实例,为什么?
—>这是因为多线程并发导致竞态条件(因为执行时序导致的并发安全问题),简单来说,就是有可能多个线程一起读到了lazy==null,然后觉得对象没有被实例化过,然后它们都跑去实例化了。
解决竞态条件一般选择加锁的方式:
// 双重检测锁模式public static Lazy getInstance() {if (lazy == null) {synchronized (Lazy.class) {if (lazy == null) {lazy = new Lazy();}}}return lazy;}
还有什么问题呢?联想一下JMM内存模型的三大特性,原子性、有序性、可见性
这里还有一个很关键的操作lazy = new Lazy();
,在进行这一步操作时,在底层并不是一步实现的,是通过多条指令来完成的,也就是说这条指令不是原子性操作,而JVM中允许一定的指令重排序来提高性能
lazy = new Lazy();
:
- 分配内存
- 执行构造方法
- 返回引用地址
假设指令重排序了,变成1->3->2,当执行到3时,已经返回结果给lazy这个单例bean了,但是实际上lazy还没有执行构造方法没有实例化,假如此刻有另外一个线程过来读取lazy,发现lazy不为空,指向一块地址,这个线程在使用这个lazy的时候可能会出现问题
假设线程A和线程B同时执行 getInstance() 方法:
1.线程A的执行流程:
- 步骤1:分配内存空间。
- 步骤3:将内存地址赋值给 lazy(此时对象未初始化)。
- 步骤2:后续执行构造函数初始化对象(可能稍后完成)。
2.线程B的执行流程:
在线程A执行到步骤3后,线程B进入 getInstance():
- 检查 lazy != null(此时 lazy 已指向分配的内存地址,但对象未初始化)。
- 直接返回 lazy 引用。
问题:线程B拿到的 lazy 对象尚未完成初始化,导致后续操作可能访问到未初始化的成员变量或方法,引发异常。
这里采用volatile关键字解决这个问题,volatile关键字禁止指令重排并保证其可见性。
private static volatile Lazy lazy;
Java里面有一个很霸道的东西叫做反射,它可以忽视修饰符的限制,所以在这里就有一个问题了,可以通过反射拿到构造器方法,然后创建实例对象,怎么解决?
@Test
public void getLazyInstance() throws Exception {// 通过反射拿到无参构造器 new个对象Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();constructor.setAccessible(true);Lazy instance1 = constructor.newInstance();Lazy instance2 = constructor.newInstance();System.out.println(instance1);System.out.println(instance2);
}
结果:
com.aof.singleton.Lazy@4dcbadb4
com.aof.singleton.Lazy@4e515669
可以想到在类中加一个字段表示是否创建了实例,通过反射创建实例是可行的,但是我们限制它只能创建一次,也就是说通过反射创建实例只有第一次能创建成功,后续创建都拒绝。
private static boolean FLAG = false;private Lazy() {synchronized (Lazy.class) {if (FLAG) {throw new RuntimeException("不要用反射");}FLAG = true;}
}
这其实又回到了原来的问题,应该反射很霸道,所以也可以通过反射修改flag的值,所以还是会有问题。
改进方案:第一次调用构造器时(无论是通过getInstance还是反射)就把当前的单例bean赋值为this
private Lazy() {synchronized (Lazy.class) {if (lazy != null) {throw new RuntimeException("禁止多次通过反射创建实例!!!");}lazy = this;}
}
完整代码实现:
// 懒汉式单例
public class Lazy {private static volatile Lazy lazy;private Lazy() {if (lazy == null) {synchronized (Lazy.class) {if (lazy != null) {throw new RuntimeException("禁止多次通过反射创建实例!!!");}lazy = this;}}}// 双重检测锁模式public static Lazy getInstance() {if (lazy == null) {synchronized (Lazy.class) {if (lazy == null) {lazy = new Lazy();}}}return lazy;}
}
测试:
@Test
public void getLazyInstance() throws Exception {// 通过反射拿到无参构造器 new个对象Constructor<Lazy> constructor = Lazy.class.getDeclaredConstructor();constructor.setAccessible(true);// 通过反射创建对象实例Lazy instance1 = constructor.newInstance();Lazy instance2 = Lazy.getInstance();System.out.println(instance1);System.out.println(instance2);
}
结果:
com.aof.singleton.Lazy@4dcbadb4
com.aof.singleton.Lazy@4dcbadb4
静态内部类模式
静态内部类模式(Static Inner Class Pattern)是单例模式的一种实现方式,通过静态内部类实现延迟加载和线程安全,同时避免了传统懒汉式单例模式中使用synchronized关键字的性能开销。
(1) 延迟加载(Lazy Initialization)
- 静态内部类在外部类加载时不会被初始化,只有当显式使用内部类时才会被加载。
- 单例对象在静态内部类的static变量中初始化,因此首次调用getInstance()时才会创建实例。
(2) 线程安全
- JVM类加载机制保证了类初始化的线程安全性:当多个线程同时访问getInstance()时,JVM会确保静态内部类Holder的初始化只执行一次,且所有线程看到的INSTANCE都是同一个实例。
public class StaticLazy {private StaticLazy() {if (LazyHolder.instance != null && LazyHolder.instance != this) {throw new RuntimeException("不要通过反射多次创建对象");}}private static class LazyHolder {private static final StaticLazy instance = new StaticLazy();}public static StaticLazy getInstance() {return LazyHolder.instance;}
}
测试:
@Test
public void getSStaticLazyInstance() {for (int i = 0; i < 10; i ++) {new Thread(()->{StaticLazy instance = StaticLazy.getInstance();System.out.println(instance);}).start();}
}
@Test
public void getSStaticLazyInstanceByReflect() throws Exception{Constructor<StaticLazy> declaredConstructor = StaticLazy.class.getDeclaredConstructor();declaredConstructor.setAccessible(true);StaticLazy instance = declaredConstructor.newInstance();StaticLazy instance1 = declaredConstructor.newInstance();System.out.println(instance);System.out.println(instance1);
}
结果:
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
com.aof.singleton.StaticLazy@36e5bf5b
枚举类型
使用枚举类型的单例是最简洁的方式,不仅能避免多线程同步问题,还能防止反序列化重新创建新的对象。
public enum EnumSingleton {INSTANCE;private EnumSingleton() {}public static EnumSingleton getInstance() {return INSTANCE;}
}
测试:
@Test
public void getInstance() {EnumSingleton instance = EnumSingleton.getInstance();EnumSingleton instance1 = EnumSingleton.getInstance();EnumSingleton instance2 = EnumSingleton.INSTANCE;System.out.println(instance.hashCode());System.out.println(instance1.hashCode());System.out.println(instance2.hashCode());
}
结果:
1305193908
1305193908
1305193908
如有错误,欢迎指正!!!