谈谈你知道的设计模式? - 《java核心技术》笔记
分类
按照模式的应用目标大致分类:
- 创建型模式:是对对象创建过程的问题和解决方案的总结,比如单例、工厂、构建器、原型。
- 结构型模式:针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验,比如适配器、装饰者、桥接、代理、组合、外观、享元。
- 行为型模式:从类或对象之间交互、职责划分等角度总结等模式。比如策略、解释器、命令、观察者、迭代器、模版方法、访问者。
例子
装饰器
通过识别类设计特征来进行判断,其类构造函数以相同的抽象类或者姐口味输入参数。
public BufferedInputStream(InputStream in)
构建器
比较优雅的解决构建复杂对象的麻烦,这里的“复杂”是指类似需要输入的参数组合较多,如果用构造函数,则往往需要为每一种可能的输入参数组合实现相应的构造函数。
HttpRequest request = HttpRequest.newBuilder(new URI(uri))
.header(headerAlice, valueAlice)
.headers(headerBob, value1Bob,
headerCarl, valueCarl,
headerBob, value2Bob)
.GET()
.build();
单例
版本一:
public class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
有问题,没有把构造函数声明为private的
版本二:private化构造函数+懒加载。
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
问题:多线程不可用,比如考虑以下情况:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 检查到uniqueSingleton为空 | |
T3 | 初始化对象A | |
T4 | 返回对象A | |
T5 | 初始化对象B | |
T6 | 返回对象B |
版本三:双检锁(Double Check Lock)+volatile
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton() {
}
public static Singleton getSingleton() {
if (singleton == null) { // 尽量避免重复进入同步块
synchronized (Singleton.class) { // 同步.class,意味着对同步类方法调用
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
volatile关键字的作用:
实例化对象的顺序可以分为:
- 分配内存空间。
- 初始化对象。
- 将对象指向刚分配的内存空间。
但是有些编译器为了性能的原因,可能会进行重排序:
- 分配内存空间。
- 将对象指向刚分配的内存空间。
- 初始化对象。
这样考虑两个线程发生了以下调用:
Time | Thread A | Thread B |
---|---|---|
T1 | 检查到uniqueSingleton为空 | |
T2 | 获取锁 | |
T3 | 再次检查到uniqueSingleton为空 | |
T4 | 为uniqueSingleton分配内存空间 | |
T5 | 将uniqueSingleton指向内存空间 | |
T6 | 检查到uniqueSingleton不为空 | |
T7 | 访问uniqueSingleton(此时对象还未完成初始化) | |
T8 | 初始化uniqueSingleton |
这样子在T7时刻线程B访问到了一个初始化为完成的对象。
volatile保证重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。
总结:
- volatile提供可见性,以保证getInstance返回的是初始化完全的对象;
- 在加锁前检查,尽量避免进入相对昂贵的同步快;
- 在class级别同步,保证现场安全的类方法调用。
版本四:使用静态内部类
理论依据是对象初始化过程中隐含的初始化锁。该模式被认为是替代双边检的最佳方式。
静态内部类的优点:外部类加载时不需要立即加载内部类,内部类不被加载则不去初始化singleton。
知识点
- clinit()和init()的区别
:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行
:在实例创建出来的时候调用,包括调用new操作符;调用Class或java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。
init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
clinit are the static initialization blocks for the class, and static field initialization.
上面这两句是Stack Overflow上的解析,很清楚init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。
class X {
static Log log = LogFactory.getLog(); // <clinit>
private int x = 1; // <init>
X(){
// <init>
}
static {
// <clinit>
}
}
- clinit的线程安全性
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行 ()方法完毕。如果在一个类的 ()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行 ()方法后,其他线程唤醒之后不会再次进入 ()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
public class Singleton {
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class Holder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static Singleton singleton = new Singleton();
}
}
问题:由于是静态内部类的形式去创建单例,所以外部无法传递参数进去。实际使用时可以在静态内部类和DCL模式中自己权衡。