单例模式 - Singleton Pattern

简介

什么是单例?为什么需要单例?
单例模式的目的是设计出一个类,能提供全局唯一的对象。
举个例子,程序中需要一个类用做管理配置项。这样一个类显然希望是全局只有一个,在任何地方都能获取和使用,任何地方使用的对象也都是同一个。这种情况就需要使用单例模式!

简单实现

举例来说,我们要写一个Singleton类的单例,首先我们先写一个类:

public class Singleton{
}

要保证类是单例的,那么就不能随意通过new操作符来随意创建实例。否则,每个new出来的对象都不是同一个对象,哪有单例可言。为了禁止new操作符创建对象,需要显式地将构造函数声明为private

public class Singleton{
    private Singleton(){}
}

既然不能随意创建,那么总得有一个方法能够返回单例对象吧,我们命名为getInstance:

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        //return a unique Singleton instance;
    }
}

重点:

  • getInstance必须是static方法,因为只有static方法才可以直接通过Class调用
  • getInstance方法提供的必须是唯一实例

那么问题来了,getInstance方法如何能提供唯一实例呢?
第一种简单的方法如下:

public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return this.instance;
    }
}

这种模式叫做饿汉模式,即一开始就创建一个对象,每次通过getInstance方法返回时,就返回这个对象。很明显,这个一定是单例的,全局只有一个Singeton对象。
重点:

instance必须是static的。因为static方法中使用的只能是static对象。(这是因为static方法是可以通过class直接调用的,而非static成员必须有instance的时候才能使用。)

饿汉模式是如何保证线程安全的?

JVM保证static成员只会被初始化一次。

缺点
“饿汉模式”有个缺点:

  1. 只要类被加载,对象就会被创建,不管有没有调用getInstance方法。如果单例对象是个大对象,没有用到但又占着内存空间,是比较浪费的。
  2. 如果单例对象的创建依赖于某些配置项,必须先配置,再创建对象,那么饿汗模式是没法使用的。

因此,衍生出另一种对应的模式“懒汉模式”

“懒汉模式”的特点是只有在使用的时候才创建对象。为了实现这种效果,很容易想到下面这种写法:

public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

关键点是,先判断instance对象有没有已经被创建过,如果等于null,说明没有被创建过,则创建一个,否则直接返回已有对象。
是不是感觉这样写没毛病?No!No!No!这样写毛病很大!因为不是线程安全的。当多个线程几乎同时检测if(instance == null)时,都发现instance为null,这时都继续往下走,从而创建了多个对象。如下图所示:

非线程安全

如何写出线程安全,并且性能良好的单例,一直是一个常见的面试题。

可能很多人可以很快给出以下方案:

public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

这个方案是通过添加synchronized关键字来同步方法。
重点:

  • 使用synchronized来实现线程安全。在不考虑性能的情况下,绝对简单、有效。

缺点:
这个方案很显然能够实现线程安全,但是性能堪忧。每次获取单例对象都要进行同步,有必要吗?试想,当一个对象创建以后,以后的所有操作都只是读,读也同步的话,很显然是影响效率的。

getInstance方法可以这样写吗?

public static Singleton getInstance(){
    synchronized(Singleton.class){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

可以!synchronized(Singleton.class)是个类锁,在static方法上加synchronized关键字同样是类锁,两者是一样的。

引申

synchronized关键字 On the way...

简单的加synchronized关键字的方案的问题主要在于锁的粒度太大。只是简单的像下面一样减小锁的粒度也是不行的:

public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

这个方案又重新引入了线程安全问题,synchronized关键字只是使并发创建对象编程了顺序进行。这个方案直接pass。那么如何既能减小锁的粒度,又能保证线程安全呢?

DCL

上面的方案减小的锁的粒度,单不是线程安全。我们可以在上面方案的基础上继续修改:

public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这个方案就是Double Check Lock(简称DCL)。这个方案在上面方案的基础上做了改进:synchronized同步块里面能够保证只创建一个对象。但是通过在synchronized的外面增加一层判断,就可以在对象一经创建以后,不再进入synchronized同步块。这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升。

现在的方案是不是已经很完美了?
并!不!是!
为什么呢?

  1. 一个单例可以通过短的更多的代码实现。
  2. DCL在极小的概率下,创建的对象不可用!会报错!

我们首先讲第二个问题,DCL为何产生的对象有可能不可用。这是instance = new Singleton()不是原子操作,而是可以大致分为以下三个步骤:

  1. Singleton对象分配内存
  2. 初始化Singleton对象
  3. 将创建的对象引用赋值给instance

单线程情况下,instance = new Singleton会严格按照上述1,2,3步骤逐次执行,并不会出错。但是在多线程情况下,由于在优化时编译器和CPU指令重排的存在,上述步骤执行的次序有可能为1,3,2,这样,有可能出现如下的情况:

指令重排

我们期望的顺序是1-2-3,但是由于指令重排,实际的顺序可能是1-3-2,这种情况下,线程1创建了一个单例对象,虽然instance已经赋值了,但是对象还没有初始化,线程2在第一次check instance是否为null的时候,得到对象已经创建的错误信息,就直接使用,显然会出错。这种情况出现概率极低,系统低负载的时候很难出现,但是高负载时,一旦出现问题,就很难调查!

那么,有什么解决方案呢?可以使用volatile关键字。volatile关键字修饰了instance后,有两个作用:

  • 禁止对instance的操作重排指令
  • 并能保证多线程之间instance对象的及时可见性。

如下:

public class Singleton{
    private static volatile Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

重点

  • 使用volatile关键字禁止指令重排。
  • 这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。

引申

详解指令重排 On the way...
详解volatile On the way...

还记不记得上面提到,单例可以有更短的代码?对!使用静态内部类!

静态内部类

使用静态内部类实现单例模式的通用写法如下:

public class Singleton{
    private Singleton(){}
    public static Singleton getInstance(){
        return InnerClass.getInstance();
    }
    private static class InnerClass{
        private static Singleton instance = new Singleton();
        public static Singleton getInstance(){
            return instance;
        }
    }
}

重点:

  • 在静态内部类中创建单例对象。JVM保证static对象的创建是线程安全的。
  • JVM还保证,一个类的任何static方法没被调用的情况下,所有的static成员都不会被加载和初始化。这样能保证单例对象只有在用的时候才会创建。
  • 依旧是懒汉模式,只有需要时才创建

内部类可以是public的吗?

不可以!我们期望只有通过调用getInstance方法才能获得单例对象,如果内部类是public的,就破坏了封装性。

instance成员可以是public的吗?

不建议。成员对象用public修饰,同样破坏了类的封装性。不过非要这么写,也没办法。

其实,还有更短的代码!!!

枚举

public enum Singleton{
    INSTANCE;
}

重点

  • 默认枚举实例的创建是线程安全的,所以不需要担心线程安全的问题。
  • 《Effective Java》中推荐的模式

总结

单例模式是一个很常用的设计模式,也是一个在面试中常被问到的设计模式,更是一个很容易答错的设计模式。
单例模式虽然看起来简单,但是设计的Java基础知识非常多,如static修饰符、synchronized修饰符、volatile修饰符、enum等。能正确地写出说容易也行,说难也行。这个难易就在于个人的理解程度!很多人面试时遇到这个问题,经常丢三落四,少些一些关键修饰符,或者不清楚怎么写。关键的点在于,不要死记硬背,一定要理解!
同时,更凸显出java基础知识的重要性!需不需要static,可不可以public,这些都是有java基础语法做为论据的。深刻理解了java语法,才能以不变应万变,才不怕忘记!

如有问题,请留言,我会及时回复,一起讨论问题。

参考

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

推荐阅读更多精彩内容