Java设计模式之-单例模式(Singleton)

先说一下我自己对单例模式的理解:

单例模式:在整个程序运行周期内,某个类被设计为其所有实例都归属于一个副本,以保证含义上的唯一性和行为上的总控性。这种类的设计方式被称为单例模式。

如果某个类从现实世界角度来看,确实应该只存在一个实例副本,或者该类的行为是作为整个系统中某个功能的总控统筹,将它通过单例模式来实现,能够提供良好的可维护性和准确性,也更节省占用的内存和新生成实例的开销。例如管理一个JDBC连接的类,或者一个Canvas中的画笔类等。

为了实现单例,首先不能让类被随意地实例化。我们可以通过创建private构造函数来屏蔽new关键字的调用。

在网上随便搜一下,能够看到五花八门的单例模式说明,各种名词层出不穷,饿汉懒汉饱汉等等...但在这篇文章中我想层层递进地说一下各种实现方式的进化关系。

基本的单例

在我刚开始写代码的时候,曾经也遇到过需要只存在一个实例的场景。当时还很懵懂,就写出了如下的代码:

// “懒汉” - 延迟初始化,非线程安全
// “懒”表现为: instance要等到真正使用时(getInstance)才会被实例化
public class Singleton1 {  
     private static Singleton1 instance;  
     private Singleton1 (){}   
     public static Singleton1 getInstance() {  
         if (instance == null) {  
             instance = new Singleton1();  
         }  
         return instance;  
     }  
}  

以上代码是非线程安全的。
试想当在多线程场景中,许多线程几乎同时调用getInstance方法,并判断instance == null为true,这个时候便会有多个现成去执行实例化代码。这样一来便无法保证类的单例了。

而另外一种比较基本的单例写法是:

//“饿汉”(其实我想称它为“勤男”) - 立即初始化,线程安全
//"饿"体现在: 饿汉很饿,希望尽早吃到内存
//"勤"体现在:人家一上来就把内存区开好了,相比于上面的“懒汉”,当然是“勤”了
public class Singleton2 {  
    private static Singleton2 instance = new Singleton2();  
    private Singleton2 (){}
    public static Singleton2 getInstance() {  
        return instance;  
    }  
}  

这种写法使用Java的语法糖,能够满足多线程的并发需求。但是从延迟初始化的角度来说,是有欠缺的。尤其是当Singleton是一个比较复杂的类,无法简单地通过new关键字进行实例化,或者需要获得某些参数才能完成实例化时,延迟初始化就成了我们的必选项。

从上面我们可以看到,两种基本的单例实现方式,都存在各自的缺点。根据上面的分析,其实我们想要的是一种既能延迟初始化又保证了多线程安全的单例实现模式。


延迟初始化+线程安全的单例

为了要保证二者兼得,我们在第一种代码的基础上,增加能够保证线程安全的代码即可。为此,我们引入synchronized关键字。

synchronized关键字

synchronized关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

由于是说设计模式的文章,这里就不展开讨论synchronized了。按照上面的描述,如果没研究过它的朋友应该也能基本了解它的作用。我们使用它来实现一个满足两个条件的单例:

//"懒汉"变种
//用 synchronized 修饰 getInstance方法
public class Singleton3 {  
     private static Singleton3 instance;  
     private Singleton3 (){}
     public static synchronized Singleton3 getInstance() {  
         if (instance == null) {  
             instance = new Singleton3();  
         }  
         return instance;  
     }  
}  

这种写法满足了线程安全,但是安全过头了。在多线程场景中,同一时刻只能有一个线程访问getInstance方法,换言之就是多线程在这个方法上变成了单线程。大家可以想到,即使后续的线程中instance已经不为null了,但还是要等待前序线程执行完该方法。这无疑是对效率的一大阻碍。

Double Check Lock (DCL)

为了提升多线程效率,我们将synchronized换了个位置。但是为了确保单例,我们又在synchronized内部增加了一次if判断,这样便有了两次null检查,即DCL:

//"懒汉"变种
//用 synchronized 修饰 getInstance方法
public class Singleton4 {  
     private static Singleton4 instance;  
     private Singleton4 (){}
     public static synchronized Singleton4 getInstance() {  
         if (instance == null) {  
             synchronized(Singleton4.class){
                 if(instance == null) {
                     instance = new Singleton4();
                 }  
             }
         }  
         return instance;  
     }  
}  

由于synchronized作用域已经从一个方法缩小到了一段代码块,多个线程可以同时访问第一个if判断,如果instance不为null便可以直接返回,不用等待。这种写法虽然奇怪,但是看起来确实实现了延时初始化和线程安全,并且提升了多线程的效率。

但是实际上,这种方式并没有保证完全的线程安全,罪魁祸首便是指令重排序

指令重排序

instance = new Singleton4();这行代码,可以被编译器编成以下三行指令:

  • rawMemory = allocateMemory(); //分配内存
  • preparedMemory = initMemory(rawMemory); //初始化内存
  • instance = preparedMemory; //内存与字段绑定

试问如果编译器将结果优化为以下序列,将后两个指令调换顺序,多线程情况下会出现什么结果?

  • 分配内存
  • 内存与字段绑定
  • 初始化内存

有的线程可能在刚进入该方法时,刚好上述指令执行到了第二步,因此instance不为null。但实际上此时instance还没有被初始化完成,线程拿到的是一个残缺的非null实例

volatile关键字

volatile关键字保证了对应的字段能够含有一下特性:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改变量值,这新值对其他线程来说是立即可见的。
  2. 禁止对操作该变量的指令重排序。

具体关于volatile关键字的说明可以参见网上其他文章,这里也不再赘述,大家只要先了解上述两个特性。下面我们来利用volatile对我们的单例模式进行进一步的加工:

//"懒汉"变种
//用 volatile 修饰 instance
public class Singleton5 {  
     private static volatile Singleton5 instance = null;  
     private Singleton5 (){}
     public static synchronized Singleton5 getInstance() {  
         if (instance == null) {  
             synchronized(Singleton5.class){
                 if(instance == null) {
                     instance = new Singleton4();
                 }  
             }
         }  
         return instance;  
     }  
}  

通过使用volatile修饰instance,保证了实例化时指令的正确顺序,也确保了多线程安全。这种写法基本上实现了一个单例的基本要求。


套路的单例

Holder模式

除了上面组合使用synchronized和volatile进行多线程安全保护外,我们还可以按照Holder方式将基本的实例中第二种“勤汉”模式进行修改,从而再实现一套即延迟初始化,又保证线程安全的代码:

//Holder模式
//引入静态类,该类在首次实际使用时进行内存分配,即return SingletonHolder.INSTANCE时
public class Singleton6 {  
     private static class SingletonHolder {  
         private static final Singleton6 INSTANCE = new Singleton6();  
     }  
     private Singleton6 (){}
     public static final Singleton6 getInstance() {  
         return SingletonHolder.INSTANCE;  
     }  
}  

枚举模式(Since jdk1.5)

由于枚举的特性,它实际上是一个天然的单例,确保了实例副本的唯一性:

public enum Singleton7{
    INSTANCE;
}

枚举型的单例能够支持线程安全,且能够实现延迟加载,并且还能防止反序列化问题和反射攻击问题,不愧为Effective Java中提出的完美的单例解决方案。


反序列化问题和反射问题

这一块内容完全是为了对单例模式进行补充,如果只是为了了解设计模式的话,可以不再往下阅读了。

由于单例模式最重要的一点就是保证该类的实例副本的唯一性,而如果这个类支持序列化,那么在反序列化的时候我便可以产生多个实例,即反序列化是一个隐藏很深的构造函数。如果不对这种情况进行封锁,势必会破坏单例。

private Singleton readResolve(){
    return getInstance();
}

通过实现readResolve方法,在反序列化时,跳过默认逻辑,而使用已经写好的getInstance方法,能够规避这种问题。

而反射问题则是利用Java的反射机制,调用到private访问权限的构造函数,从而生成了多个实例。针对这个问题,目前可以看到的是枚举模式能够完美地进行处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容