设计模式(三) 单例模式

单例模式也是一种创建型模式,而且也非常容易理解:在一个系统中可能需要多个配置文件,我们希望这些配置文件的实例只存在一个,而不是存在多个重复的实例。这时候就需要使用单例模式。

单例模式有几个要点:

  • 一是必须确保只存在一个类的实例。
  • 二是类必须自己创建自己,不允许其他类来创建自己。
  • 三是必须提供一个方法允许其他类访问单例成员。

根据这些特点,我们可以很容易猜出单例类在Java的样子:首先他的构造方法必须是私有的,然后往往需要一个公有的静态方法获取单例实例。

单例的实现

单例模式的实现有很多种,按照单例的实例化的时机可以分为饿汉式和懒汉式两种,下面来逐一说明。

懒汉式(非线程安全)

这种方式非常简单,也很容易理解。单例实例在第一次调用的时候才创建,符合懒加载的要求。唯一缺点是这种方式不支持多线程,在多线程环境下可能会创建多个对象。

public class UnThreadSafeSingleton {
    private UnThreadSafeSingleton() {
    }

    private static UnThreadSafeSingleton singleton;

    public static UnThreadSafeSingleton getSingleton() {
        if (singleton == null) {
            singleton = new UnThreadSafeSingleton();
        }
        return singleton;
    }
}

懒汉式(同步的)

我们可以对上面的实现方式进行改进,以便在多线程环境下也可以正常工作。实现方式很简单,直接在方法上添加synchronized关键字即可。

这种实现方式虽然也很简单,但是性能不咋地。由于直接在方法上加了锁,所以如果同时有两个地方获取单例对象,其中一个就会阻塞。在获取单例的次数获取比较多的时候性能很差。

public class SynchronizedThreadSafeSingleton {
    private static SynchronizedThreadSafeSingleton singleton;

    private SynchronizedThreadSafeSingleton() {

    }

    public synchronized static SynchronizedThreadSafeSingleton getSingleton() {
        if (singleton == null) {
            singleton = new SynchronizedThreadSafeSingleton();
        }
        return singleton;
    }
}

饿汉式(静态初始化)

如果不要求必须懒加载,那么我们可以使用JVM的类加载工作机制,方便的实现单例模式。

JVM在第一次加载类的时候,会被初始化累的静态域,并确保静态域只初始化一次。所以我们可以将创建单例的代码放到静态初始化块中,这样JVM会帮我们创建单例。这种方式的缺点就是加载类的时候就创建了单例对象,没有懒加载。

public class FirstLoadSingleton {
    private static FirstLoadSingleton singleton;

    private FirstLoadSingleton() {
    }

    static {
        singleton = new FirstLoadSingleton();
    }

    public static FirstLoadSingleton getSingleton() {
        return singleton;
    }
}

双检锁方式

这种方式比较复杂,但是其他方面都很好:既实现了懒加载,同时也是线程安全的,性能还不错。

双检锁模式的要点:一是单例必须使用volatile关键字标记;二是在创建单例的时候要进行两次检查(这就是双检锁的含义)。我们可以看到同步块在第一次判断之后,也就是说只有在第一次调用时才可能发生竞争和阻塞。单例创建之后,在获取单例的时候不会调用同步块,因此速度会非常快。和前面的直接在方法上添加同步的例子相比,真是不知道高到哪里去了。

public class DoubleCheckLockSingleton {
    private volatile static DoubleCheckLockSingleton singleton;

    private DoubleCheckLockSingleton() {
    }

    public static DoubleCheckLockSingleton getSingleton() {
        if (singleton == null) {
            synchronized (DoubleCheckLockSingleton.class) {
                if (singleton == null) {
                    singleton = new DoubleCheckLockSingleton();
                }
            }
        }
        return singleton;
    }
}

静态内部类方式

这种方式和双检锁方式的效果类似,既可以保证懒加载又具有多线程下的性能优势。而且实现起来更加简单。唯一缺点就是单例对象必须是静态的,而双检锁方式的单例对象可以是实例的。

道理也很简单,如果我们把单例放到类的静态字段上,不能保证延迟加载的话,那么再用一层内部类包住不就行了。这样,当外层类第一次加载的时候,不会触发单例的初始化。而在第一次获取单例的时候,才会调用内部类,从而让JVM加载单例。

public class InnerClassSingleton {
    private static class Inner {
        private static InnerClassSingleton singleton = new InnerClassSingleton();
    }

    private InnerClassSingleton() {
    }

    public static InnerClassSingleton getSingleton() {
        return Inner.singleton;
    }
}

枚举方式

这种方式是Java实现单例最好的方式,连《Effective Java》都推荐我们使用这种方式。不过现在貌似使用的还是比较少。一来,枚举是Java 1.5才加入的东西;二来,Java的枚举使用起来确实很捉急。甚至有些开发实践都要求不使用枚举,而是使用共有静态字段来代替。所以枚举单例这种方式就比较稀少了。

不过确实,Java的枚举天生就是为实现单例而存在的。首先,枚举的实例是在使用时才被初始化的,这和单例模式延迟加载的要求相符。其次,枚举类型只允许存在私有的构造函数,从根本上杜绝了创建多个单例的可能性。而且当枚举序列化和反序列化的时候,同样会保证单例的唯一性。因此我们说,枚举方式是Java实现单例最好的方式。

可能还是不太好理解,所以还是直接看代码吧。假设我们需要一个单例的配置对象,我们可以创建枚举来解决。枚举的构造方法默认(且只能)是私有的,我们直接在构造方法中初始化数据(例如从文件读取等等),然后通过枚举类中定义的方法来读取数据。如果对Java的枚举还是感觉到比较陌生的话回去复习一下枚举类的用法。

public enum EnumSingleton {
    Instance;

    private String data;

    EnumSingleton() {
        //在构造方法中进行初始化
        data = "Some data";
    }

    public String getData() {
        return data;
    }
}

当然在现在的Java生态中单例模式一般不需要我们手动实现了。像Spring和Guice这样的依赖注入框架已经实现了单例模式,所以我们在使用这些框架的时候,创建和确保单例的工作有这些框架完成,我们只需要编写传统的非线程安全类即可。

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

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,222评论 4 34
  • 1 场景问题# 1.1 读取配置文件的内容## 考虑这样一个应用,读取配置文件的内容。 很多应用项目,都有与应用相...
    七寸知架构阅读 6,640评论 12 68
  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,485评论 1 8
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 今天是世界旅游日,给孩子们翘课一天,带小姑娘们出去撒个欢! 感谢这个节日,这座城市好多景点都免费,于是早早就约好今...
    Williamlora阅读 382评论 0 0