Java设计模式百例 - 单例模式

本文源码见:https://github.com/get-set/get-designpatterns/tree/master/singleton

单例模式(Singleton Pattern)是Java中最简单的设计模式之一,但也是一个很值得玩味儿的设计模式,这是一个创建型的模式。

单例模式的目的在于,对于一些类,需要保证其仅有一个实例。比如一个Web中的计数器,不用每次刷新在数据库里记录一次,可以用一个单例的对象先缓存起来。但是这个计数器不能有多个实例,否则岂不是不准了。

具体来说,该设计模式有如下特点或要求:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例。

例子

饿汉式

我们考虑一下上述的三个要求。

首先,单例类只能有一个实例。我们知道,一个类的static的成员变量或方法可以看作归属于类本身的,可以被这个类所有的实例共享,可以认为是”独一份儿“的。所以用static关键字修饰这个类的对象,就可以做到。

然后,单例类必须自己创建自己的唯一实例。那也好办,单例类自己定义一个类型为自己的成员变量,然后设置为static的就可以了。然后把构造方法设置为private的,这样就不能被其他类或对象new了。

最后,单例类必须给所有其他对象提供这一实例。那就是提供一个get方法可以获取到这个类型为自己的成员变量。

分析过后,先撸一版代码:

SingleObject.java

public class SingleObject {
    private final static SingleObject INSTANCE = new SingleObject();
    private SingleObject() {}
    public static SingleObject getInstance() {
        return instance;
    }
}

我们搞一个Client检验一下:

Client.java

public class Client {
    public static void main(String[] args) {
        System.out.println(SingleObject.getInstance());
        System.out.println(SingleObject.getInstance());
    }
}

返回结果:

com.getset.designpatterns.singleton.SingleObject@5e2de80c
com.getset.designpatterns.singleton.SingleObject@5e2de80c

根据对象的hash可以看出,两次调用getInstance()返回的是同一个对象。

这种是比较常用的方式,叫做“饿汉式”,为啥叫这名自己体会哟~ 我们知道,类加载时,static的成员变量会初始化,所以一旦类加载,那么INSTANCE所指向的对象就被创建了。其实这就是个常亮了,所以可以用大写,然后final修饰。

懒汉式

在有些情况下,如果单例类本身比较消耗资源或加载缓慢,希望能够在使用的时候才创建实例,那么可以采用懒加载的方式,如下:

LazySingleObject.java

// 本例是线程不安全的
public class LazySingleObject {
    private static LazySingleObject instance;
    private LazySingleObject() {}

    public static LazySingleObject getInstance() {
        if (instance == null) {
            instance = new LazySingleObject();
        }
        return instance;
    }
}

如此,只有在调用getInstance()的时候才会创建实例。

但是这种方式是线程不安全的,假设有两个线程同时调用了getInstance(),可能都会检测到instance == null。所以可以把getInstance()方法使用synchronized修饰,以便保证线程安全。

但是在实际使用过程中,一旦实例被创建后,getInstance()方法只是返回实例,是不需要同步的,而加锁会影响效率,因此我们考虑不对整个方法加锁,而仅仅只对new实例的过程加锁,如下:

LazySingleObject.java

// 双检锁/双重校验锁(DCL,即 double-checked locking)
public class LazySingleObject {
    // 使用volatile修饰单例变量
    private static volatile LazySingleObject instance;
    private LazySingleObject() {}
    public static LazySingleObject getInstance() {
        // 第一次判断,此时是未加锁的
        if (instance == null) {
            synchronized (LazySingleObject.class) {
                // 第二次判断,此时是线程安全的
                if (instance == null) {
                    instance = new LazySingleObject();
                }
            }
        }
        return instance;
    }
}

可见,只有在实例未创建(instance == null)的时候才同步创建实例对象。在synchronized代码库内部,再次检查实例是否创建,因为第一次检查并不是线程安全的。也因此,这种方式叫做“双检锁/双重校验锁”。这样,一旦实例创建之后,就不再进入同步代码块了,从而效率更高。

要特别注意的是,单例变量instance要用volatile进行修饰。原因在于编译器出于优化需要会对内存的读写操作重排序,因此LazySingleObject对象初始化时的写操作与写入instance字段的操作可以是无序的。导致的结果就是如果某个线程调用getInstance()可能看到instance字段指向了一个LazySingleObject对象,但看到该对象里的字段值却是默认值,而不是在LazySingleObject构造方法里设置的那些值。而使用volatile字段修饰后,编译器和运行时都会注意到这是个共享变量,因此不会将该变量上的操作和其他内存操作一起重排序,真正保证线程安全。

以上两种方式可根据场景选择。

登记式/静态内部类

如果感觉“双检锁/双重校验锁”这种方式复杂难学,可以采用静态内部类来实现线程安全的懒加载。

LazyHandlerSingleObject.java

public class LazyHandlerSingleObject {
    private static class SingleObjectHandler {
        private final static LazyHandlerSingleObject instance = new LazyHandlerSingleObject();
    }
    private static LazyHandlerSingleObject instance;

    public static LazyHandlerSingleObject getInstance() {
        return SingleObjectHandler.instance;
    }
}

我们知道,类在使用的时候才会被classloder加载,静态内部类SingleObjectHandler正是利用了这个特点,来懒加载其外部对象LazyHandlerSingleObject的实例。

类嵌套一向是一种比较难以理解的概念,静态内部类是最简单的一种嵌套类,最好把他看作是普通的类,只是碰巧被声明在另一个类内部而已。唯一与普通类不同的是,静态内部类和其外部类可以互相访问所有成员,包括私有成员。

当第一次调用getInstance()方法的时候,SingleObjectHandler才第一次被使用到,从而加载它,自然也就创建了LazyHandlerSingleObject的实例,SingleObjectHandler通过static保证这个实例是唯一的。

枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

这种方式是《Effective Java》作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。推荐在需要使用单例的时候使用这种方式。

EnumSingleton.java

public enum EnumSingleton {
    INSTANCE;
    public void doSomething() {
        ...
    }
}

就是这么简单!直接使用EnumSingleton.INSTANCE就可以了,比如EnumSingleton.INSTANCE.doSomething()。Enum类型类似于类,也可以有成员变量和成员方法。

总结

首先,请尝试使用枚举的方式来实现单例,枚举机制本身对单例有很好的支持。

如果觉得枚举方式不熟悉,那么:

通常,比较轻量的单例直接用饿汉式即可;

重量级的单例对象,最好通过懒加载的方式构建,根据线程安全性的要求选择以上两种懒汉式的方式;当然静态内部类也是一种不错的懒加载方式。

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

推荐阅读更多精彩内容