大家好,我是此林。
设计模式为解决特定问题提供了标准化的方法。在项目中合理应用设计模式,可以避免重复解决相同类型的问题,使我们能够更加专注于具体的业务逻辑,减少重复劳动。设计模式在定义系统结构时通常考虑到未来的扩展。例如,工厂模式、策略模式等能让系统在增加新功能时无需改动现有代码,只需扩展新模块即可,减少了修改现有代码的风险。
今天分享的是单例模式和模板模式,这两种设计模式在项目和源码中的使用。
1. 单例模式
一般开发中,我们使用的是 Spring 框架,默认情况下,我们通过 @Bean
、@Component
等注解注入的 Bean 对象是 单例的(即 Singleton),也就是说 Spring 会在容器启动时创建一个该类型的 Bean 实例,并在整个应用程序上下文中共享这个实例。可以通过 @Scope
注解来指定 Bean 的作用域,控制其生命周期和作用范围。默认的作用域是 @Scope("singleton")。
那 Spring 是如何实现单例模式的呢?
关注源码,发现 Spring 维护了一个全局的单例池(ConcurrentHashMap),key 是 BeanName,value 是 Bean 对象。
(DefaultSingletonBeanRegistry
类实现了 SingletonBeanRegistry
接口)
我们在开发过程中使用Bean对象,会根据 BeanName 去单例池中获取 Bean 对象,保证了对象的全局的唯一性。
当然,它和我们平时所说的几种单例模式实现还是不一样的。
1. 饿汉式
public class Singleton {private static final Singleton instance = new Singleton();private Singleton() {}public static Singleton getSingleton() {return instance;}
}
关键点:
1. 使用 private 关键字,代表外部无法对变量 instance 直接修改
2. 使用 static 关键字,代表 instance
变量在类加载的时候就会被初始化,这个和 JVM 加载类有关。
3. final
关键字的作用:
final
用于修饰变量时,表示该变量 一旦赋值就不能再被修改。也就是说,变量 引用 一旦指向某个对象,就不能再指向其他对象。- 当
final
修饰一个引用变量时,它指向的对象不能改变。但是,引用的对象本身是可以改变的,也就是 对象内部的状态是可以修改的。
4. 私有化构造方法。也就是防止外部通过 new
关键字创建多个实例。
5. 最后一个 getSingleton() 方法是提供全局访问点,返回唯一实例。
6. 在类加载时就初始化实例,避免线程安全问题。缺点是无论是否使用该实例,都会创建一个实例,浪费内存资源。
2. 双重检查锁定(懒加载)
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getSingleton() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;} }
关键点:
1. 由于 instance 用了 static 修饰(类级别的变量),且没有初始化,那么类加载的时候 instance 赋值为 null。
2. 不加 final ,是因为后续 instance 需要的时候会被赋值;如果加了 final ,那么 instance 永远只能指向 null。当然哈,jdk 是不允许加了 final 的变量为 null 的,会直接编译错误。
3. 加上 volatile 关键字,是保证多线程下的内存可见性。即:一个线程修改了 instance 的值,另一个线程马上就能看到,也就是强制读主内存,不读工作内存。
4. 私有化构造方法。同上,防止外部通过 new 创建多个实例。
getSingleton() 详解:
其实去掉第一个 if 判断也可以,也就是这样:
public static Singleton getSingleton() {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}return instance;}
1. 加上第一个 if 的好处是:
无锁判断,instance 不为空直接返回,为 null 再加同步锁,提高性能,避免每次获取都加锁。
2. 加了锁之后为什么还要判断呢?
试想这么一个场景:两个线程同时来了,都发现 instance 为 null,线程A先获取了锁创建了对象,那么线程B获取锁后无需创建对象,所以要在判断一次是否为 null。
3. 静态内部类(懒加载)
public class Singleton {private Singleton(){}private static class SingletonHolder {private static final Singleton instance = new Singleton();}public static Singleton getSingleton() {return SingletonHolder.instance;}
}
使用静态内部类的方式实现单例,JVM 会在加载外部类时延迟加载内部类,既能实现懒加载,又能避免多线程问题。
4. 枚举类
public enum Singleton {INSTANCE;public void test() {}
}
其他所有的实现单例的方式其实是有问题的,那就是可能被反序列化和反射破坏。
枚举的写法的优点:
- 不用考虑懒加载和线程安全的问题,代码写法简洁优雅
- 线程安全
反编译任何一个枚举类会发现,枚举类里的各个枚举项是是通过static代码块来定义和初始化的,它们会在类被加载时完成初始化,而java类的加载由JVM保证线程安全,所以,创建一个Enum类型的枚举是线程安全的
- 防止破坏单例
我们知道,序列化可以将一个单例的实例对象写到磁盘,然后再反序列化读回来,从而获得一个新的实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建类的一个新的实例,相当于调用该类的构造函数。
Java对枚举的序列化作了规定,在序列化时,仅将枚举对象的name属性输出到结果中,在反序列化时,就是通过java.lang.Enum的valueOf来根据名字查找对象,而不是新建一个新的对象。枚举在序列化和反序列化时,并不会调用构造方法,这就防止了反序列化导致的单例破坏的问题。
对于反射破坏单例的而言,枚举类有同样的防御措施,反射在通过newInstance创建对象时,会检查这个类是否是枚举类,如果是,会抛出异常java.lang.IllegalArgumentException: Cannot reflectively create enum objects
,表示反射创建对象失败。
5. 模板模式
实现模板方法通常有两步:
1. 抽象类:定义模板方法和抽象方法,在模板方法里会调用抽象方法。
2. 子类:继承抽象类,重写抽象方法。子类运行时调用父类的模板方法,模板方法运行时再去调用子类重写的抽象方法。
源码应用(AQS,Reentrantlock)
1. AQS 的模板方法定义
AQS 是基础的抽象类,提供通用的同步机制。它的 acquire() 和 release() 方法是模板方法。AQS 中的 tryAcquire() 和 tryRelease() 抽象方法,定义了获取锁和释放锁的具体逻辑。
2. ReentrantLock.lock() 源码
这里 ReentrantLock.lock() 内部就是调用 AQS 的模板方法 acquire(),1 表示要获取一个锁,后续 state 会加1。
这是 AQS 的模板方法,其中 tryAcquire(arg) 方法由子类 ReentrantLock 重写实现。
AQS 作为一个抽象类,除了被 ReentrantLock 继承,还被 CountDownLatch、Semaphore继承。所以说,AQS 提供通用的 模板方法,提高了代码的复用性。