关于单例模式,一直是面试场上高级面试题,通过这道题目能看出来应聘者应对高并发问题的技术能力。随着互联网的发展,单例模式的实现方式也在发生着变化,下面来一一列举各种实现方式,来讨论着极高的并发情况使用何种方式实现单例模式。
懒汉式
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
顾名思义,就是比较懒,在使用的时候采取创建对象。这种实现方式只适用于单线程或少量线程的情况下,稍微有多线程首次并发请求就会出现同时创建多个单例对象的错误。
饿汉式
public class SingletonHungry {
private static SingletonHungry instance = SingletonHungryHelper.result;
private static class SingletonHungryHelper{
public static SingletonHungry result = new SingletonHungry();
}
private SingletonHungry(){}
public static SingletonHungry getInstance(){
return instance;
}
}
这种方式比上一种方式稍微好点,即在第一次访问的时候就创建好对象。但是如果第一次有并发访问,还是会出现创建出多个单例对象的错误。
以上两种模式,包括此类型衍生的静态信息代码块模式都无法解决高并发情况下同时创建多个单例对象的问题。能解决此问题的方法是什么呢?我们很自然的就想到了加锁,于是就有下面几种实现方式。
加锁
public class SingletonLock {
private static SingletonLock instance;
private SingletonLock(){}
public static SingletonLock getInstance(){
synchronized (SingletonLock.class){
if(instance == null) {
instance = new SingletonLock();
}
}
return instance;
}
}
这种实现方式有一个明细的缺点,所有的线程都在等一把锁,效率低。
改进加锁:
public class SingletonLock {
private static SingletonLock instance;
private SingletonLock(){}
public static SingletonLock getInstance(){
if(instance == null){
synchronized (SingletonLock.class){
instance = new SingletonLock();
}
}
return instance;
}
}
这种方式虽然细粒化了锁的范围,单是也是有问题的。例如:A,B,C三个线程并发访问,三个线程都在等待synchronized锁,其中A抢到了锁,创建单例对象。B,C两个线程就会等待,等到A创建完成后,释放锁,B或C就会抢锁,然后继续创建单例对象。这就会造成创建了多个单例对象的错误。因此正确的实现方式是在下面:
DCL式,双验证加锁:
public class SingletonDcl {
private static SingletonDcl instance;
private SingletonDcl(){}
public static SingletonDcl getInstance(){
if(instance == null){
synchronized (SingletonDcl.class){
if(instance == null){
instance = new SingletonDcl();
}
}
}
return instance;
}
}
在线程抢到锁之后,还要继续验证单例对象是否创建。如果已创建就迅速释放锁,未创建时再去创建对象。
但是这样就能满足要求了吗。某大厂一个面试题,DCL式下要不要加上volatile关键字,如下模式。
public class SingletonDcl {
private static volatile SingletonDcl instance;
private SingletonDcl(){}
public static SingletonDcl getInstance(){
if(instance == null){
synchronized (SingletonDcl.class){
if(instance == null){
instance = new SingletonDcl();
}
}
}
return instance;
}
}
这里要不要加上volatile关键字,我们要从java对象创建过程和CPU乱序执行两个方面考虑。
java对象创建过程可分为:
1:内存中分配空间
2:初始化对象
3:变量与对象关联
这三个过程,正常请客下按照1,2,3这个顺序执行没有问题,但是CPU是乱序执行的,关于CPU乱序执行请参考我的博客:https://blog.csdn.net/swangooth/article/details/105278725
万一某个过程CPU执行的顺序变成了:1,3,2,即把“变量与对象关联”放在“初始化对象”之前执行了,这样创建的单例对象还未被初始化就被线程使用了,使用的结果也是有各种问题。当然这种情况出现的概率非常低,在某些极高的并发情况下才会出现。
而volatile关键字的作用是:
1、禁止线程重排序,即禁止CPU乱序执行。
2、保持内存可见性。
恰恰能够解决在某些极高并发情况下单例对象创建错误的问题。
volatile原理参考:https://blog.csdn.net/swangooth/article/details/105278725