单例模式
一个类最多只有一个实例, 并且限制只能有一个实例的设计模式就是"单例模式". 这里的限制主要是通过编程语言本身的语法特性, 强行限制某个类只能实例化一个对象.
static关键字就是天然的好手, 使用static修饰后, 对象就变成类对象, 类对象其实就是单个实例, 只会存在一个.
单例存在的具体解释: 类对象是通过JVM的.class文件加载获得的, .class文件只会加载一次, 所以也就只有一个类对象, 类对象上面的static成员也就只有一份了. 类属性在整个进程中都是独一无二的. (类对象是以进程为单位的?)
单例模式的基本形式如下:
class SingleDog {
private static SingleDog singleDog = new SingleDog();
public static SingleDog getSingledog() {
return singleDog;
}
private SingleDog() {}
}
创建对象的时候就通过private和static创建, static保证对象是属于类的, private修饰构造方法, 防止在类外被调用创建新的实例, 只能通过调用公开静态的getSingledog才能获得唯一单例.
"饿汉模式"和"懒汉模式"
上代码, 直接在类加载阶段就创建好了单例的实例, 就是"饿汉模式"的例子.
反之"懒汉模式"就是不那么着急, 在有需要的时候再创建单例.如下:
class SingleLazyDog {
private static SingleLazyDog singleLazyDog = null;
public static SingleLazyDog getSingleLazyDog() {
if (singleLazyDog == null) {
singleLazyDog = new SingleLazyDog();
}
return singleLazyDog;
}
private SingleLazyDog () {}
}
懒汉模式可以避免再初期初始化时候占用过多的资源.
如何在满足线程安全的前提下创建单例类?
"饿汉模式"在类加载阶段就创建了单例对象, 过后在调用getter时候只有读取操作, 没有修改操作(这里的修改指的是创建), 所以"饿汉模式"天然是线程安全的.
但是"懒汉模式就不一样了", 首先上面这种写法, 一方面是有获取比较的过程, 一方面是有创建实例的过程(既涉及读也涉及写), 这一波操作都不是原子的, 所以可能会在多个线程同时调用getter方法时候出现冲突, 导致创建多个实例, 破坏了单例模式的规则.
最简单粗暴的形式是直接加锁:
class SingleLazyDog {
private static SingleLazyDog singleLazyDog = null;
public static SingleLazyDog getSingleLazyDog() {
synchronized (SingleLazyDog.class) {
if (singleLazyDog == null) {
singleLazyDog = new SingleLazyDog();
}
}
return singleLazyDog;
}
private SingleLazyDog () {}
}
在一个线程调用getter的时候, 直接对类对象加锁, 就不会发生两个线程同时实例化的结果, 这样确实保证了线程安全. 但是也出现了新的问题, 就是导致调用getter的时候都需要进行一次加锁开锁的操作. 加锁的开销可能会涉及用户态到内核态的切换, 即成本是相对较高的, 能避免还是尽量避免. 解决方法也简单, 就是在需要的时候加锁, 不需要的时候不加呗:
class SingleLazyDog {
private static SingleLazyDog singleLazyDog = null;
public static SingleLazyDog getSingleLazyDog() {
if (singleLazyDog == null) {
synchronized (SingleLazyDog.class) {
if (singleLazyDog == null) {
singleLazyDog = new SingleLazyDog();
}
}
}
return singleLazyDog;
}
private SingleLazyDog () {}
}
看似是已经解决问题了, 但实际上对于有实例化对象(new)操作的步骤, 还会存在"指令重排序"的问题.
我们需要先了解new操作的步骤, 本质上大概是三个:
- 申请内存, 得到该内存的地址
- 调用构造方法初始化实例
- 把内存首地址赋值给实例引用
出现指令重排序问题, 主要是因为这个程序操作和内存IO有关, 和内存IO有关, 编译器就可能会进行自主优化, 比如"指令重排序", 比如使用"工作内存"等等.
这里的2步骤和3步骤, 要是2和3调换了顺序, 就相当于内存确实被创建好了, 但是没有对应的实例, 是个空有的无效的内存空间.
解决这个问题, 用个volatile修饰singleLazyDog就好
private volatile static SingleLazyDog singleLazyDog = null;