程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

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

发布于2021-05-29 22:34     阅读(509)     评论(0)     点赞(28)     收藏(0)


[版权申明] 非商业目的注明出处可自由转载
博文地址: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



所属网站分类: 技术文章 > 博客

作者:我爱编程

链接:http://www.javaheidong.com/blog/article/207651/8751bab179b2d4126d00/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

28 0
收藏该文
已收藏

评论内容:(最多支持255个字符)