前言:
上次面试虽然被一家神仙公司坑了,但是在那些很简单的题中还是有一个我不太会的东西,也是之前一直有看见却没有去学习的点。这个点就是【设计模式】中常用的其中一个【单例】。展开了一下后发现还能涉及线程安全的问题..嗯,内容足以写一个博客了。
一些辅助阐述的小标注(第二版):
(* ***) = 老子拿头来保证就是我是说的这个意思
(? ***) = 很有可能不对的理论,我偷懒没做任何验证。是个大胆的假设,可能会坑了你
(@ ***) = 参考资料中 原文or大义or原文例子
(# ***) = 是我用常见的自然语言或例子对原文的翻译、解析
【***】 = 我想让读者注意的名词
(***) = 我的口水。如果是英文,则是对左侧的名词翻译,对标特定的名词,避免阅读出现歧义。
背景介绍-单例模式
单例模式指的是应用(application)在整个生命周期内,只能存在一个且是同一个对象(往后成为"实例”)。不明白的可以去随便new 两个实例并输出它们的hashCode值。
实现的形式
1 饿汉模式(类似静态类)
2 懒汉模式(懒加载)
饿汉模式的单例跟【静态类】有很多相似之处,这里就不说静态类了,简单说一下单例的好处。
1单例可以继承类,实现接口,而静态类不能(可以集成类,但不能集成实例成员)。
2单例可以被延迟初始化,静态类一般在第一次加载是初始化。
3单例类可以被集成,他的方法可以被覆写;
4最后,或许最重要的是,单例类可以被用于多态而无需强迫用户只假定唯一的实例。举个例子,你可能在开始时只写一个配置,但是以后你可能需要支持超过一个配 置集,或者可能需要允许用户从外部从外部文件中加载一个配置对象,或者编写自己的。你的代码不需要关注全局的状态,因此你的代码会更加灵活。
饿汉模式
这就是一个饿汉模式的单例模式。所谓的饿汉模式,即是立即加载。换句话说,就是按以上的设计,在调用者调用 getInstance();得到实例之前,该实例其实早就被初始化了。
这样的缺点也非常明显,当该单例内容非常多的时候,会占用很多资源,而且这种浪费是有办法可以避免。
懒汉模式
其实就是延迟加载(也就是懒加载LazyLoad),其设计可以让实例在被调用者调用其实例化函数的时候才被创建。这样就避免了内存被浪费。
非常简单就不写备注了。
模拟线程安全问题
但是,在多线程的场景里,以上的懒汉模式的设计是非线程安全的。
Let me show you
测试代码(开了5个异步线程)
测试代码输出。可以看到,每个线程获取的实例的哈希值都不一样。也就是说不是同一个对象。这明显不符合单例模式的初衷。
解决线程安全问题
其实解决方法很简单如果为了性能不用同步锁(synchronized),那可以加一个判断,判断实例是否已被初始化。但是以上代码还是不能解决问题,why? (模拟业务代码耗时并不是单纯为了拖时间,也模拟了多线程非并行执行的资源调度机制)
因为我们都知道,线程虽然不是并行执行的,但是譬如有两个线程A和B,A获得执行权进入了IF块后执行权却从线程A给到线程B,B也进入了IF块。so,说到这里都懂了吧?
来,真正地解决一下问题。有两种方式(当然不是只有两种,只是我目前只会两种啦..)
-NO1
给代码加上类级别或者函数级别的同步锁,这种是最简单同时也是副作用最大的方式(体验在效率)。问题虽然解决了,但是却浪费了很多性能,聪明的你可能已经想到解决办法。我猜猜,"只给实例化对象的代码加同步锁"。Congratulations!你很聪明!但是你或许还漏了点东西,所以才有 NO2
-NO 2
这叫DCL双重检查锁机制。
这回总算解决问题了,真正地实现了单例模式。
结语:
DCL双重检查锁不是最好的解决办法(实际上很多资料都不推荐它,指着DCL来骂..)所以这里就看看单例的内容就好了..这一次没有像上次说的那样写理论的东西,下一次也不会。因为最近想看多一点应用层的知识。