轻松手写单例模式的6种实现方式!再也不怕面试官问了!

之前分享:
高频Java基础面试题
高频Java集合面试题

今天分享一些面试高频的单列模式的6种实现方式。

整理分享不易,先点赞评论收藏支持一波再看鸭~

文章目录:

一、单例模式的定义

定义: 确保一个类只有一个实例,并提供该实例的全局访问点。

这样做的好处是:有些实例,全局只需要一个就够了,使用单例模式就可以避免一个全局使用的类,频繁的创建与销毁,耗费系统资源。

二、单例模式的设计要素

  • 一个私有构造函数 (确保只能单例类自己创建实例)
  • 一个私有静态变量 (确保只有一个实例)
  • 一个公有静态函数 (给使用者提供调用方法)

简单来说就是,单例类的构造方法不让其他人修改和使用;并且单例类自己只创建一个实例,这个实例,其他人也无法修改和直接使用;然后单例类提供一个调用方法,想用这个实例,只能调用。这样就确保了全局只创建了一次实例。

三、单例模式的6种实现及各实现的优缺点

(一)懒汉式(线程不安全)

实现:

public class Singleton {
     private static Singleton uniqueInstance;

     private Singleton() {

    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

说明: 先不创建实例,当第一次被调用时,再创建实例,所以被称为懒汉式。

优点: 延迟了实例化,如果不需要使用该类,就不会被实例化,节约了系统资源。

缺点: 线程不安全,多线程环境下,如果多个线程同时进入了 if (uniqueInstance == null) ,若此时还未实例化,也就是uniqueInstance == null,那么就会有多个线程执行 uniqueInstance = new Singleton(); ,就会实例化多个实例;

(二)饿汉式(线程安全)

实现:

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        return uniqueInstance;
    }

}

说明: 先不管需不需要使用这个实例,直接先实例化好实例 (饿死鬼一样,所以称为饿汉式),然后当需要使用的时候,直接调方法就可以使用了。

优点: 提前实例化好了一个实例,避免了线程不安全问题的出现。

缺点: 直接实例化好了实例,不再延迟实例化;若系统没有使用这个实例,或者系统运行很久之后才需要使用这个实例,都会操作系统的资源浪费。

(三)懒汉式(线程安全)

实现:

public class Singleton {
    private static Singleton uniqueInstance;

    private static singleton() {
    }

    private static synchronized Singleton getUinqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

}

说明: 实现和 线程不安全的懒汉式 几乎一样,唯一不同的点是,在get方法上 加了一把 锁。如此一来,多个线程访问,每次只有拿到锁的的线程能够进入该方法,避免了多线程不安全问题的出现。

优点: 延迟实例化,节约了资源,并且是线程安全的。

缺点: 虽然解决了线程安全问题,但是性能降低了。因为,即使实例已经实例化了,既后续不会再出现线程安全问题了,但是锁还在,每次还是只能拿到锁的线程进入该方***使线程阻塞,等待时间过长。

(四)双重检查锁实现(线程安全)

实现:

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }  
}

说明: 双重检查数相当于是改进了 线程安全的懒汉式。线程安全的懒汉式 的缺点是性能降低了,造成的原因是因为即使实例已经实例化,依然每次都会有锁。而现在,我们将锁的位置变了,并且多加了一个检查。 也就是,先判断实例是否已经存在,若已经存在了,则不会执行判断方法内的有锁方法了。 而如果,还没有实例化的时候,多个线程进去了,也没有事,因为里面的方法有锁,只会让一个线程进入最内层方法并实例化实例。如此一来,最多最多,也就是第一次实例化的时候,会有线程阻塞的情况,后续便不会再有线程阻塞的问题。

为什么使用 volatile 关键字修饰了 uniqueInstance 实例变量 ?

uniqueInstance = new Singleton(); 这段代码执行时分为三步:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

正常的执行顺序当然是 1>2>3 ,但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。
单线程环境时,指令重排并没有什么问题;多线程环境时,会导致有些线程可能会获取到还没初始化的实例。
例如:线程A 只执行了 1 和 3 ,此时线程B来调用 getUniqueInstance(),发现 uniqueInstance 不为空,便获取 uniqueInstance 实例,但是其实此时的 uniqueInstance 还没有初始化。

解决办法就是加一个 volatile 关键字修饰 uniqueInstance ,volatile 会禁止 JVM 的指令重排,就可以保证多线程环境下的安全运行。

优点: 延迟实例化,节约了资源;线程安全;并且相对于 线程安全的懒汉式,性能提高了。

缺点: volatile 关键字,对性能也有一些影响。

(五)静态内部类实现(线程安全)

实现:

public class Singleton {

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }

}

说明: 首先,当外部类 Singleton 被加载时,静态内部类 SingletonHolder 并没有被加载进内存。当调用 getUniqueInstance() 方法时,会运行 return SingletonHolder.INSTANCE; ,触发了 SingletonHolder.INSTANCE ,此时静态内部类 SingletonHolder 才会被加载进内存,并且初始化 INSTANCE 实例,而且 JVM 会确保 INSTANCE 只被实例化一次。

优点: 延迟实例化,节约了资源;且线程安全;性能也提高了。

(六)枚举类实现(线程安全)

实现:

public enum Singleton {

    INSTANCE;

    //添加自己需要的操作
    public void doSomeThing() {

    }

}

说明: 默认枚举实例的创建就是线程安全的,且在任何情况下都是单例。

优点: 写法简单,线程安全,天然防止反射和反序列化调用。

  • 防止反序列化
    序列化:把java对象转换为字节序列的过程;
    反序列化: 通过这些字节序列在内存中新建java对象的过程;
    说明: 反序列化 将一个单例实例对象写到磁盘再读回来,从而获得了一个新的实例。
    我们要防止反序列化,避免得到多个实例。
    枚举类天然防止反序列化。
    其他单例模式 可以通过 重写 readResolve() 方法,从而防止反序列化,使实例唯一重写 readResolve() :
private Object readResolve() throws ObjectStreamException{
        return singleton;
}

四、单例模式的应用场景

应用场景举例:

  • 网站计数器。
  • 应用程序的日志应用。
  • Web项目中的配置对象的读取。
  • 数据库连接池。
  • 多线程池。
  • ......

使用场景总结:

  • 频繁实例化然后又销毁的对象,使用单例模式可以提高性能。
  • 经常使用的对象,但实例化时耗费时间或者资源多,如数据库连接池,使用单例模式,可以提高性能,降低资源损坏。
  • 使用线程池之类的控制资源时,使用单例模式,可以方便资源之间的通信。

以上就是单例模式的6中实现方式了,好好理解一下,下次面试问这个就一点不虚了!

看完之后,如果还有什么不懂的,可以在评论区留言,会及时回答更新。

最后,记得点赞评论收藏支持一波鸭~

#Java#
全部评论
感谢参与【创作者计划2期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz2jsghtlq(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复 分享
发布于 2021-03-13 10:18
很有收获,期待下集
点赞 回复 分享
发布于 2021-03-14 21:35
今天仔细拜读了这篇文章。小菜鸟想请问,是不是可以这么理解: 基本上可以分为两种思路,一种是饿汉式一种是懒汉式。 其中饿汉式有:1.直接创建好实例,等调用的时候就给直接返回(就是方法2普通饿汉式)。2.方法5相当于改进版的饿汉,只有外部调用了创建实例的方法,我才会给他创建,如果不调用,那么我就不创建。会比普通饿汉式稍微好一点。 其中懒汉式:方法1的普通懒汉式。方法3的懒汉加锁。方法4的改进版懒汉加锁。
点赞 回复 分享
发布于 2021-03-16 18:03

相关推荐

01-15 20:43
已编辑
四川大学 Java
1、你觉得你哪些点是比较好想让我看到的吗说了下实习经历,公司有天出事跟着加班到12点2、问了下一个进程启动启动不了怎样排除,回答可以看系统调用 strace 来排查,举了下之前遇到vnc启动不了的例子,发现是某个依赖的.so文件没有。追问:还有没有其它方法回答:这个不太清楚,可能有其它的日志吧面试官:有个demsg能够看3、机器中毒你是怎样排查的回答:top、crontab、systemctl、netstat、lastb 后面清楚病毒发现入口是一台Hadoop机器说下僵尸进程 (忘了僵尸进程跟孤儿进程的差别了 寄 https://www.cnblogs.com/yychuyu/p/15553400.html) 怎样看进程父进程(扯了下systemctl status pid 后续搜了下还可以使用pstree查看)4、说下进程的状态及转变5、怎样在linux暂停进程(不清楚,只回答知道kill,但应该也有类似pause、resume的命令)7、计算机网络说下三次握手,为什么不两次握手,timewait状态时什么时候,如果timewait状态过多怎样排查8、说下你的项目有哪些亮点 说了基因算法、一些设计模式9、12306你有没有进行感谢 扯了一些串行的使用多线程改造了,效果要好写10、idea手撕快排(五六分钟手撕出来,之前敲过好多次了,idea写就是爽) 快排时间复杂度 想要倒序排改哪里11、idea手撕全排列(搜索+回溯 秒了) 如果想要三永远都在2前面怎么办(加了个flag,测试没问题)12、开始问我还有没有其它亮点 :开始扯自己博客(感觉有个博客确实还是有好处) 问我lc刷了多少题(说hot100基本刷完了) 问我 自己平时怎么学习的 (带着问题学习,博客里面很多todo,学习过程就是不断的增加todo和减少todo)13、说下对递归的理解 (霹雳巴拉扯了一堆)后续反问:做什么的、实习生进来干什么感觉整场面试难道挺高的,问了好多linux,可能自己吹linux挺熟练的,手撕的话因为是idea写还是爽面试官人也挺友善的
查看28道真题和解析
点赞 评论 收藏
分享
2024-12-28 18:39
已编辑
安徽大学 Java
牛客2024-10-18投的,不久后会发笔试链接,笔试内容有选择题 多选题 还有算法题和数据结构设计题我记得。11-13 一面拷打我的弱鸡秒杀项目,被打烂了,我题都没听懂,大脑自动删除了这段记忆(依稀记得和库存有关);一些八股,时间有点久了,好像问了threadlocal;sql题:店铺商品金额查询每个商品的金额都大于500的店铺名称我的答案:SELECT DISTINCT 店铺 FROM 店铺商品表t1 WHERE NOT EXISTS (SELECT 1FROM 店铺商品表t2WHERE t1.店铺=t2.店铺 AND t2.金额<=500); 后面复盘感觉时间复杂度偏高,可以使用group by和having的;算法:给两个整数数组nums1和 nums2,返回两个数组中公共的、长度最长的连续的子数组的长度。示例1:输入:nums1 =[1,2,3,2,1],nums2= [3,2,1,4,7]输出: 3解释:长度最长的公共子数组是[3,2,1]。示例2:输入:nums1 = [0,0,0,0,0], nums2= [0,0,0,0,0]输出: 5我的答案:class Solution {    public int findLength(int[] nums1, int[] nums2) {        int m = nums1.length;        int n = nums2.length;        // dp[i][j] 表示以nums1[i-1]和nums2[j-1]结尾的最长公共子数组长度        int[][] dp = new int[m + 1][n + 1];        int maxLen = 0;                // 初始化第一行和第一列都是0,不用显式初始化,Java数组默认值就是0                // 填充dp数组        for (int i = 1; i             for (int j = 1; j                 if (nums1[i-1] == nums2[j-1]) {                    dp[i][j] = dp[i-1][j-1] + 1;                    maxLen = Math.max(maxLen, dp[i][j]);                }                // 如果当前元素不相等,那么以这两个元素结尾的公共子数组长度为0                // 不用显式设置,因为默认值就是0            }        }                return maxLen;    }}11-15 二面,应该是主管面问我研究方向,研究背景,我的模型什么的,研一的三篇paper;继续拷打我的弱鸡秒杀项目(QwQ);八股:mvcc;算法题:不重复子串问题。11-29 三面,CEO面12-3 好像是中午人力聊了一会(口头offer),下午发的正式offerps:字节挂了痛苦死我了#日常实习后端# #面经#
查看7道真题和解析
点赞 评论 收藏
分享
01-13 18:34
已编辑
美团_JAVA后端(准入职员工)
    首先说一下鼠鼠本人个人情况,鼠鼠本人四川某双非一本高校,老家也是四川的,目前是25届应届生。     因为并不打算考研,所以去年三月的时候就意识到了找工作找实习的重要性,于是开始了实习投递。。。由于时间久远,所以具体时间已经记不清了。因为鼠鼠双非本的原因,在成都找开发实习boss上几乎已读不回,当时只有一家C++的面试,是盛大游戏的游戏客户端开发实习岗,第一次面试没经验,遂挂。    在此之后受到打击,于是转投测试岗,也陆陆续续面了几家,全都挂掉,索性硬着头皮去网申大厂开发。很幸运,鼠鼠凭借学校实验室导师的项目成功被🐧选中,去到了深圳总部QQ的移动端开发(现在看来还是很感谢🐧带给我的人生转变)。     从此在🐧开启了长达六个月的实习之路。承担组内最累的活,几乎下最晚的班,每天晚上十一点多坐上公司班车回去的时候,看着窗外的街景都会幻想自己转正后的日子。无奈事与愿违,all in转正的结果果然很惨烈,由于竞争实在太大,最后还是被淘汰了。     其实也不是全身心all in,在转正考核快来临的时候也有其他家hr找过我,接下来介绍鼠鼠的辛苦找工作历史。————————————————————————————    由于有了🐧的背书,在去年八月份的时候就有字节hr找我约面试了。以下的面试经历纯属鼠鼠的倾诉,所以按照时间线排布(不一定准),不按照公司归类。——八月字节一面,是成都抖音的移动端开发,我很开心,因为是成都的岗位,顺利的话可以回家了。但是由于实习忘了太多基础,也没有时间准备面试,所以面得很差,挂了——八月被深圳的抖音移动端开发捞了,同样原因挂掉——九月底被告知转正失败,十月份开始看秋招机会,陆续投了各大厂,因为双非本+入场时间晚,基本没什么机会——十月份主动去企微找各个组ld或者总监询问是否有校招hc自荐,由于是秋招了,所以我把地点限制在了成都,还是想回家。自荐也确实收到了很多面试邀请。——十月份基础微信移动端开发一面挂——十月份企业微信安卓开发二面挂——十一月份天美l1王者荣耀项目组移动端开发二面挂——十一月份滴滴某事业部iOS开发一面挂——十一月份小天才安卓开发hr面挂(可能要太高了,第一次被问薪资没准备好)——十一月份陌陌安卓开发一面,无下文——十一月小红书iOS移动端开发一面挂(怀疑kpi)——十一月初蔚来实习offer,担心重蹈覆辙,拒。——十一月份中国石油西南油气田数智化,笔试挂(这岗位竞争太离谱了,亏我准备这么久行测)——对自己很失望,把眼光降低了,投向了成都的小公司——十一月份成都领之锋offer,实习转正,开得低拒了——十一月份成都鼎桥offer,后边了解到是华为外协,996薪资也低,明明头的秋招,给我改成了实习转正,拒——十一月底微信输入法团队移动端开发一面挂——十二月份滴滴新的部门安卓开发,二面挂——十二月份北京汽车之家iOS面试通过,后续审批offer告知老板突然想要社招,停止推进了——十二月份北京知乎安卓开发offer,薪资不太满意,而且在北京要求尽量提前实习,入职时间定在了4.28,奈何手上没其他家offer,接了保底了——十二月份tme,全民k歌二面挂,面试挺好的,不清楚为什么挂了——十二月份tme,QQ音乐移动端开发一面挂(同面挺好的不清楚挂的原因)——十二月PDD客户端一面一来就手撕,面试官提出各种改进自己也不知道可不可行,怀疑是kpi,一面挂——十二月中旬想着到成都找个小公司实习先苟着等春招,很幸运找了个小公司——十二月底收到了成都字节(飞书前端)和成都美团(Java后端)的面试邀请。同时收到了MiniMax和小黑盒的——一月初,小公司老板发现我老请假觉得我不稳定把我开了不过他们对实习生要求也真是高,比正式员工要求还高,说是当骨干培养,但是稍微做不好就开人——一月初字节三面挂(说是职业生涯规划与团队不匹配——一月初晚上做饭的时候很惊喜收到了团子的oc,没想到跳过了hr面,当时没有一点准备,很惊喜。oc完十多分钟就发下来了offer,第二天开始了三方签约。这个部门不强制提前实习,不过他们很欢迎提前实习去看看,我也准备去体验三个月。冬招顺利收官。————————————————————————————    秋招途中投递了很多大厂,上述没写出来的要么是小公司,或是没回音或者简历没过的。确实测评笔试也做得想吐,手撕leetcode中等、hard、cpp手写线程池都经历过。不过身为一个双非本鼠鼠,也有为了找工作拼命的准备。    塞翁失马,焉知非福。希望还在冬招或者准备春招的同学们不要放弃,祝愿大家也能获得满意的Offer,提前祝大家新年快乐!———————-1.13更新———————-了解到团子这是个sp,打算春招也放弃了,做纯纯的团孝子 #一句话证明你在找工作#  #秋招你被哪家公司挂了?#  #牛客创作赏金赛#
点赞 评论 收藏
分享
评论
11
63
分享

创作者周榜

更多
牛客网
牛客企业服务