聊聊并发1:单例模式
0.单例模式用途
单件模式属于工厂模式的特例,只是它不需要输入参数并且始终返回同一对象的引用。
单件模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。
1.单例模式分类
单例模式可以分为懒汉式和饿汉式
饿汉式单例模式:在类加载时就完成了初始化,所以类加载比较慢,但获取对象的速度快。比如:
No.1 饿汉式
/*
* 1.饿汉式
*这种方式基于classloder机制避免了多线程的同步问题,
*不过,instance在类装载时就实例化,
*这时候初始化instance显然没有达到lazy loading的效果
*
* */
public class Singleton1 {
private static Singleton1 instance = new Singleton1();// 直接初始化一个实例对象
private Singleton1() {// private 类型的构造函数 抱着其他对象不能直接new一个该对象的实例
}
public static Singleton1 getInstance() {// 该类唯一的一个public方法
return instance;
}
}
那如何提升加载速度呢?我们就需要用到懒汉式的单例模式了。首先我们能想到的就是如下所示的这种做法,首先 不初始化一个实例对象,等拿到是空时再去初始化,但是它有一个问题:在多线程不能正常工作,原因就是:判断是不是为空和实例化对象不是原子操作。
No.2 懒汉,线程不安全
/*
* 懒汉模式,线程不安全
*
* */
public class Singleton2 {
private static Singleton2 instance;
private Singleton2(){}
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
懒汉模式在使用时,容易引起不同步问题,所以我们很自然的想到用synchronized关键字创建同步"锁"如下:
No.3 懒汉,线程安全
/*
* 懒汉,线程安全
*
* */
public class Singleton3 {
private static Singleton3 instance;
private Singleton3(){}
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
这种做法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是效率很低(因为锁),因为在任何时候只能有一个线程调用 getInstance() 方法。但是同步操作只需要在第一次调用时才被需要,即第一次创建单例实例对象时。这个时候我们可以用双重同步锁来解决这个问题。它用同步块加锁的方式对代码块进行加锁操作。
No.4 双重校验锁
/*
* 双重校验锁
*
* */
public class Singleton4 {
private volatile static Singleton4 instance;
private Singleton4() {
}
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
有人会问:为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
注意:这里的 instance 变量是被声明成 volatile 。为什么?
因为instance = new Singleton4()
并不是原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情,这个在我以前的文章里也说了:
- 1.首先是会给 instance 分配内存
- 2.调用 Singleton 的构造函数来初始化成员变量
- 3.将instance对象指向分配的内存空间。
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
那怎么怎么应对,这里我们只需要将 instance 变量声明成 volatile 就可以了。
原因是:volatile关键字除了可以使得变量在不同线程间具有可见性,而且可以阻止JVM将指令重排序。
那还有没有一种方法既不用加锁,也能实现懒加载。用静态内部类。
No.5 静态内部类
/*
* 静态内部类
*
* 既不用加锁,也能实现懒加载。
*
* */
public class Singleton5 {
private Singleton5(){}
private static class SingletonHolder{
private static final Singleton5 instance = new Singleton5();
}
private static Singleton5 getInstance(){
return SingletonHolder.instance;
}
}
这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种方式不同的是:第三种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三方法就显得更合理。