回字有四种写法,那你知道单例有五种写法吗

基本介绍

单例模式(Singleton)应该是大家接触的第一个设计模式,其写法相较于其他的设计模式来说并不复杂,核心理念也非常简单:程序从始至终只有同一个该类的实例对象。

举一个耳熟能详的例子,比如LOL中的大龙,一场游戏下来无论如何只有一只,所以该类只能被实例化一次。再举一个我们应用程序开发中常见的例子,Spring框架中的Bean作用范围默认也是单例的。

我相信大家都知道单例的两种最基本的写法:饿汗式和懒汉式。但是这两种写法都有其弊端所在,除了这两种写法外其实还有几种写法。此时耳边仿佛听到孔乙己的声音:

“对呀对呀!......回字有四样写法,你知道么?”。

我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子........

大家先别着急走,回字的四样写法没必要知道,单例的五种写法还是有必要晓得滴,其他的不说,至少面试的时候还能和面试官吹下是不,况且这几种写法也不是纯吊书袋,了解过后还是能帮助我们理解其设计思想滴。所以接下来咱们由浅入深,从最容易的写法开始,一步一步的带大家掌握单例模式!

写法介绍

饿汉式

话不多说,先直接上最简单的写法,然后咱再慢慢剖析:

public class Signleton01 {
    // 私有构造函数,防止别人实例化
    private Signleton01(){}
    // 静态属性,指向一个实例化对象
    private static final Signleton01 INSTANCE = new Signleton01();
    // 公共方法,以便别人获取到实例化对象属性
    public static Signleton01 getINSTANCE() {
        return INSTANCE;
    }
}

单例模式三元素

一个单例模式就这样写完了,简直不要太简单。 类里面一共就三个元素:

  1. 私有构造函数,防止别人实例化
  2. 静态属性,指向一个实例化对象
  3. 公共方法,以便别人获取到实例化对象属性

这三个元素就是单例模式的核心,单例无论哪种写法,都离不开这三个元素

这三个元素也很好理解,别人想要用我这个类的实例对象就只能通过我提供的getINSTANCE(),他想new也new不了第二个对象,自然而然就保证了该类只有唯一对象。我们可以做个试验,跑100个线程同时获取该类的实例对象,然后打印出对象的hashCode,看看到底是不是获取的同一个对象:

public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            System.out.println(Signleton01.getINSTANCE().hashCode());
        }).start();
    }
}

结果如下:

...
834649078
834649078
834649078
834649078
834649078
...

嗯,全部都是同一个对象。

优缺点

优点:写法简单,线程安全

缺点:消耗资源,即使程序从没有用到过该类对象,该类也会初始化一个对象出来

所以为了解决饿汗式的这个缺点, 我们就引出了第二种写法,懒汉式!

懒汉式

基本写法

public class Singleton02 {
    // 私有构造函数,防止别人实例化
    private Singleton02() {}
    // 静态属性,指向一个实例化对象(注意,这里没有实例化对象哦)
    private static Singleton02 INSTANCE;
    // 公共方法,以便别人获取到实例化对象属性
    public static Singleton02 getINSTANCE() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }

}

懒汉式的和饿汗式最大的区别是什么呢,就是只有在调用getINSTANCE的时候,才会创建实例,如果你从来没调用过,那么就不实例化对象。这个就比饿汗式更加节约资源,不过这种写法并不是懒汉式的完善写法,它有一个非常大的问题,就是线程不同步!我们可以按照之前那种方式创建100个线程测试一下结果:

...
1851261656
868907500
988762476
1031371881
593800070
...

可以看到这线程一同时拿,拿的都不是同一个对象,这完全就破坏了单例模式。因为很多线程在对象没有初始化前就进入到了if (INSTANCE == null)判断语句块里,自然而然就会new出不同的对象了。要解决这个线程不安全问题,就得上线程锁!

synchronized写法

public class Singleton02 {

    private Singleton02() {}

    private static Singleton02 INSTANCE;
    
    // 注意,这里静态方法加了synchronized关键字
    public synchronized static Singleton02 getINSTANCE() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton02();
        }
        return INSTANCE;
    }
}

当我们在静态方法加上synchronized关键字后,就可以保证这个方法在同一时间只会有一个线程能成功调用,也就顺理成章的解决了线程不安全问题。我们还是测试一下:

...
1226880356
1226880356
1226880356
1226880356
1226880356
...

不管多少个线程,拿到的都是同一个对象,达到了单例的要求!

优缺点

懒汉式连基本的线程安全都不能保证,就不做讨论了,我们这里主要说的事synchronized写法

优点:写法简单,节约资源(只有需要该对象的时候才会实例化)

缺点:耗性能

要知道每一次调用getINSTANCE()方法时都会上锁,这是非常耗性能的。那么为了解决这个好性能的问题,我们又引申出接下来的一种写法。

双重检测

每一次调用getINSTANCE()方法都会上锁,这是完全没有必要的嘛,因为只有对象还没有实例化的时候我才需要上锁以保证线程安全,对象都实例化了,自然也不用担心后续的调用会new出新的对象。 所以我们这个锁,可以加在if (INSTANCE == null)判断语句块里面:

public class Singleton03 {
    private Singleton03() {}

    private static Singleton03 INSTANCE;

    public static Singleton03 getINSTANCE() {
        if (INSTANCE == null) {
            // 只有在对象还没有实例化的时候才上锁
            synchronized (Singleton03.class) {
                INSTANCE = new Singleton03();
            }
        }
        return INSTANCE;
    }
}

这样就能节约一些性能,但是这样并没有做到线程安全哦! 因为很多线程进入到if (INSTANCE == null)判断语句后,虽说是因为锁不能同时new对象了,但是如果锁一旦释放,那么其他线程依然会执行到INSTANCE = new Singleton03()语句,从而破坏了单例。所以在synchronized代码块内还要加一层判断:

public class Singleton03 {
    private Singleton03() {}
    
    // 注意,使用双重检验写法要加上volatile关键字,避免指令重排(有个印象就行,这不是本文的重点)
    private static volatile Singleton03 INSTANCE;

    public static Singleton03 getINSTANCE() {
        if (INSTANCE == null) {
            // 只有在对象还没有实例化的时候才上锁
            synchronized (Singleton03.class) {
                // 额外加一层判断
                if (INSTANCE == null) {
                    INSTANCE = new Singleton03();
                }
            }
        }
        return INSTANCE;
    }
}

synchronized代码块外面一层判断,里面一层判断,就是有名的双重检测(DCL)了!里面的这一层判断加了之后呢,第一个线程的锁一旦释放也不用担心了,因为此时对象已经实例化,后续的线程也执行不了new语句,从而保证了线程安全!

优缺点

优点:节约资源(只有需要该对象的时候才会实例化)

缺点:写法复杂,耗性能(还是上了锁,还是耗性能)

虽然双重校验比synchronized懒汉式写法减少了很多锁性能消耗,但毕竟还是上了锁,所以为了解决这个锁性能消耗问题了,又引申出下一种写法。

内部类

话不多说,直接上代码:

public class Singleton04 {
    // 老套路,将构造函数私有化
    private Singleton04() {}
    // 声明一个内部类,内部类里持有实例的引用
    private static class Inner {
        public static final Singleton04 INSTANCE = new Singleton04();
    }
    // 公共方法
    public static Singleton04 getINSTANCE() {
        return Inner.INSTANCE;
    }
}

这个写法非常像饿汉式写法,单例三元素还是那三元素,只不过多加了一个内部类,将实例引用放到内部类里而已。为啥要这样写呢?因为JVM保证了内部类的线程安全,即一个内部类在整个程序中不会被重复加载,并且如果你没有使用到内部类的话,是不会加载这个内部类的。这就非常巧妙的实现了线程安全以及节约资源的好处!

优缺点

优点:写法简单、节约资源(只有调用了getINSTANCE()方法才会加载内部类,才会实例化对象)、线程安全(JVM保证了内部类的线程安全)

缺点:会被序列化或者反射破坏单例

这个缺点可以说是吹毛求疵,因为之前所有写法都会被序列化、反射破坏单例。虽然说是吹毛求疵,但咱们搞技术的还是得做到了解全部细节,我来演示一下怎样破坏这个单例

通过反射破坏单例

public static void main(String[] args) throws Exception {
    // 创建100个线程同时访问实例
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            System.out.println(Singleton04.getINSTANCE().hashCode());
        }).start();
    }

    // 反射破坏单例
    Class<Singleton04> clazz = Singleton04.class;
    // 拿到无参构造函数并将其设置为可访问,无视private
    Constructor<Singleton04> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    // 创建对象
    Singleton04 singleton04 = constructor.newInstance();
    System.out.println("反射:" + singleton04.hashCode());
}

运行结果如下:

...
2115147268
2115147268
反射:1078694789
2115147268
2115147268
...

如果是通过正常的访问实例方法,是完全可以做到单例的要求,但是如果用反射的形式来创建一个对象,则就破坏了单例,一个程序中就出现了多个不同的实例对象。那么为了解决这个吹毛求疵的问题,聪明的前辈们想到了一个完美的写法!

枚举

// 注意,这里是枚举
public enum Singleton05 {
    // 实例
    INSTANCE;
    // 公共方法
    public static Singleton05 getINSTANCE() {
        return INSTANCE;
    }
}

哎嘿,不是说所有单例都是那三元素吗,这里怎么只有两个元素呀!这是因为枚举就没有构造方法,自然而然就做到了私有化构造函数的效果,而且比私有化构造函数效果更好!因为都没有构造函数了,连序列化和反射都破坏不了这种写法的单例!!

眼见为实,我们做个试验:

public static void main(String[] args) throws Exception {
    // 创建100个线程同时访问实例
    for (int i = 0; i < 100; i++) {
        new Thread(() -> {
            System.out.println(Singleton05.getINSTANCE().hashCode());
        }).start();
    }

    // 反射破坏单例
    Class<Singleton05> clazz = Singleton05.class;
    // 拿到无参构造函数并将其设置为可访问,无视private
    Constructor<Singleton05> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    // 创建对象
    Singleton05 singleton05 = constructor.newInstance();
    System.out.println("反射:" + singleton05.hashCode());
}

运行结果如下:

...
422057313
422057313
422057313
422057313

Exception in thread "main" java.lang.NoSuchMethodException: Singleton05.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)

当运行到反射那一块代码的时候,程序直接报错,原因就是我之前所说的一样,枚举没有构造方法,你自然就无法通过反射来创建对象了!

优缺点

此方法乃是最完美的方法,真是佩服想出这种写法的前辈!

总结

五个写法全部介绍完毕,每个写法都有其特点,根据自己的需求来写就好了!每种写法理解其特点后,写出来也就非常轻松。就像我一开始说的一样,理解这五种写法也不是吊书袋,每一种写法都有其背后的思考,有些写法思路真的让人叹服,至少我了解到内部类和枚举写法的时候我心里就是:我靠!这都能想出来,太牛逼了吧......

好的代码就是艺术作品,希望我们都能码出好的艺术出来!

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