秒懂设计模式之单例模式(Singleton Pattern)

本文详细介绍了Java中的单例模式,包括5种实现方式:静态常量、单null检查、双重null检查、静态内部类和枚举。每种方式的线程安全性、性能以及优缺点进行了分析,特别强调了volatile关键字在双重null检查中的作用。此外,还讨论了如何防止反射破坏单例以及序列化时的注意事项。最后推荐使用静态内部类或枚举实现单例,因其兼顾线程安全和懒汉特性。
摘要由CSDN通过智能技术生成

[版权申明] 非商业目的注明出处可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/117266347
出自:shusheng007

设计模式汇总篇,一定要点赞收藏:

永不磨灭的设计模式(有这一篇真够了,拒绝标题党)

概述

单例模式是GOF 23种设计模式中最简单的一种,但是使用却一点不少,上帝果然喜欢简洁。单例模式虽然简单,但是也还是有很多可以探讨的地方。

咱们就来聊聊Java中单例模式的5种写法吧,以及各种设计模式的优劣,最后讨论一下你喜欢哪一种,为什么?

类型

创建型(creational)

难度

1颗星

定义

某个类只有一个实例,且自行实例化并向整个系统提供此实例

你是不是觉的终于有一个可以看得懂的设计模式的定义啦?如果是这样,说明你原本对它就比较熟悉。

使用场景

当你希望整个系统运行期间某个类只有一个实例时候

UML

这里有一张图,看见了吗?图在你心中

实例

楚中天(外号林蛋大)在外包公司干了快两年了,这期间都是外派王二狗公司的。两年中蛋大总有种寄人篱下的感觉,这不王二狗看蛋大表现很好,就向公司推荐将其转为正式。公司会通过面试来对新员工定级,下面是他们之间的对话:

面试官:林蛋大,是叫林蛋大吧?

楚中天:内心活动:蛋大nmb,瞎吗?小学毕业了吗?老子叫楚中天!脱口而出:老师,其实我叫楚中天,您可以叫我中天。

面试官:哦,好的,蛋大。你能谈谈单例模式吗,你能用几种方法实现单例模式,他们之间都有什么利弊吗?

楚中天:balabala…
在这里插入图片描述

单例模式两个核心尿点:

  • 如何保证单例

多线程环境下如何保证系统中只有一个实例?类实现序列化时如何保证?如何保证不能通过反射创建新的实例?

  • 如何创建单例

这块又分为懒汉模式饿汉模式

其实也很好理解,懒汉的意思就是这个类很懒,只要别人不找它要实例,它都懒得创建。饿汉模式正好相反,这个类很着急,非常饥渴的要得到自己的实例,所以一有机会他就创建了自己的实例,不管别人要不要。

单例模式的5种写法:

1: 静态常量

这个简单粗暴,在类加载时候就创建了实例,属于饿汉模式。其是线程安全的,这一点由JVM来保证,但是有一个缺点,可以通过反射创建新的实例。如果让你改进,你怎么弄呢?评论区留下你的见解

这里原来有一笔误,感谢评论区小伙伴的指正

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }
    
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}

2: 单null检查

使用这个写法的程序员应该说水平不是太高,这种写法应该被抛弃。其不是线程安全的,也就是说在多线程环境下,系统中有可能存在多个实例。除此之外,和上面一样通过反射也可以创建新的实例。

public class Singleton2 {
    private static Singleton2 instance;
    private Singleton2() {
    }

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

针对线程不安全问题,让我们尝试改进一下它:

我们将实例化过程放到同步块里可以解决问题吗?如下所示

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

很可惜,这种方式也是不行的,让我们简单分析一下为什么不行。

假设我们有两个线程 T1与T2并发访问getInstance方法。当T1执行完if (instance == null)且instance为null时,其CUP执行时间被T2抢占,所以T1还没有创建实例。T2也执行if (instance == null),此时instance肯定还为null,T2执行创建实例的代码,当T1再次获得CPU执行时间后,其从synchronized处恢复,又会创建一个实例。

那我们之间将同步基本升级到获取实例的方法基本可以吗?恭喜你,可以!但是,又是该死的但是!但是程序的性能被极大的降低了。下面的Singleton3 给获取实例的方法添加了synchronized。这样的话,线程是安全了,但是却极大的降低了性能,因为大部分情况下线程都只是去获取这个实例,但现在却要排队。

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

那有没有既不降低性能又保证线程安全的方法呢?Java5后答案是肯定的,因为现在Java5已经绝迹江湖了,所以可以说答案是肯定的。

3: 双重null检查

为了解决上面单null检查的线程安全与程序性能的问题,出现了double-check的方式。此方式的关键一个点就在于volatile关键字,其阻止了虚拟机指令重排,使得我们的双检查得以实现。在Java5之前,这种双重检查的方式即使加上了volatile也没有用,还是不能用,因为JVM有bug。

所以double-check方式一定要加volatile关键字,否则由于指令重拍会导致单例失败。关于volatitle可以参考秒懂Java并发之volatile关键字引发的思考

public class Singleton4 {
    private static volatile Singleton4 instance;
    private Singleton4() {
    }

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

我最开始接触到这种double-check的写法时觉得很奇诡,为什么要搞那么多check?后来我明白了:

第一重check为了提高访问性能。因为一旦实例被创建,所有的check永远为假。其实你把第一重check去掉也没问题,只是访问性能降低了,那样就变成和直接同步方法一样了。

第二重check是为了线程安全,确保多线程环境下只生成一个实例。具体分析可以参考单check部分。第一重ckeck可以被多个线程进入,但是第二重check却只能排队进入

4: 静态内部类

这种方式其实很棒,既是线程安全的,也是懒汉式的,那个实例只有在你首次访问时候才会生成。我们完全可以使用这种方式替换double-check方式。

public class Singleton5 {
    private Singleton5() {
    }

    private static class SingletonInstance {
        private final static Singleton5 INSTANCE = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

5: 枚举

最牛逼的其实是这哥们儿,以上所有方式均存在一个问题,即通过反射的方式可以创建多个实例。如果你的类实现了序列化,那还要防止序列化生成多个实例的问题。而枚举保证了线程安全,保证了反射安全,保证了序列化…

但是,但是,但是实际项目中却很少有人用enum来实现单例…

public enum Singleton6 {
    INSTANCE;
}

总结

5种实现单例模式的方式已经聊完了,除了使用枚举,你有办法防止反射破坏单例这个问题吗?小伙伴们踊跃发言,留言区咱们讨论一下

GitHub源码地址design-patterns

  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值