之前有朋友私信我说源码很难读,不知道怎么读。其实有一部分原因是因为我不了解一些源码实现套路,也就是设计模式,所以我才会写这篇文章。本文总结了源码中9种非常常见的设计模式,并列出了很多源码实现示例。希望对您阅读源码和日常工作有所帮助。
单例模式是指一个类在一个进程中只有一个实例对象(但不一定,比如spring中的Bean的单例就表示它在容器中是单例)
单例模式创建分为饥饿中国式和懒惰中国式。总共大约有8种写法。不过,开源项目中最常用的主要写法有两种:
静态常量方法属于Hungry风格,以静态变量的形式声明对象。这种单例模式在Spring中使用得比较多。例如,在Spring中,有一个用于Bean名称生成的类AnnotationBeanNameGenerator,它是一个单例。
除了上述之外,还有一种双重检查机制,这也是开源项目中常用的,也是面试中经常被问到的。双重检查机制是一种惰性风格,代码如下:
公共 类 单例 { 私有 易失性静态单例实例; 私有 单例() { } 公共 静态单例getInstance() { if(实例 == null ) { 已同步(单例.类) { if(实例 == ) null) { 实例 = 新 Singleton(); } } } 返回 实例; }}
这种方式叫双重检查机制,主要是在创建对象的时候进行了doubleINSTANCE == null的判断。
这里解释一下双重检查机制的三个疑问:
内部判断null的作用:防止对象被多次创建。假设AB同时走到同步代码块。 A先抢到锁,输入代码,创建对象,释放锁。此时B进入代码块。如果不判断为null,则直接再次创建对象,因此不是单例。是的,所以需要判断null,防止重复创建单例对象。
volatile关键字的作用:防止重新排序。因为创建对象的过程不是原子的,所以大致分为三个步骤
假设在不使用 volatile 关键字的情况下发生重排序,则执行的第二步和第三步交换,即先将 INSTANCE 变量指向 Singleton 对象的内存地址,然后再初始化该对象。这样,当并发发生时,另一个线程通过第一个if非空判断,发现不为空,就直接返回该对象,但这个对象还没有初始化。内部属性可能全部具有空值。一旦使用,很可能会出现空指针等问题。
在dubbo的spi机制中获取对象时,有这么一段代码:
虽然这段代码写法和上面的单例有点不同,但不难看出实际上是使用了双重检查机制来创建对象并确保该对象是单例的。
将复杂对象的构造与其表示分离,以便相同的构造过程可以创建不同的表示。这种设计模式称为构建器模式。它将一个复杂的对象分解为多个简单的对象,然后逐步构建。
上面的意思看起来很混乱。其实在实际开发中,建造者模式其实用的比较多。例如,有时创建pojo对象时,可以使用构建器模式来创建:
上面的代码通过构建器模式构建了一个PersonDTO对象,因此构建器模式也被称为Budiler模式。
此模式在创建对象时看起来更加优雅。当构建参数较多时,适合使用构建器模式。
接下来我们看看构建器模式在开源项目中是如何使用的
我们都知道Spring在创建Bean之前,会将每个Bean声明封装成对应的BeanDefinition,而BeanDefinition又会封装很多属性,所以为了更优雅的创建BeanDefinition,Spring提供了BeanDefinitionBuilder构建器类。
在项目中,如果我们需要使用本地缓存,我们会使用本地缓存实现框架来创建一个。比如使用Guava创建本地缓存时,我们会这样写
Cache 缓存 = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.MINUTES) .maximumSize (200) .build();
这实际上是建造者模式。
构建器模式不仅用在开源项目中,也用在JDK源代码中,比如StringBuilder类。
最后,上面提到的构建器模式实际上是Java中的简化方式。如果你想了解传统的Builder模式,可以看这篇文章