或许你曾为了应对面试而对设计模式问题做了单点突破,甚至可以做到徒手写单例;那么今天的你是否还记得如何写一个单例,什么样的写法是安全可靠的单例呢?作为设计模式的入门级模式,我们如何避免死记硬背,做到深刻理解并掌握它的用法。
通过本场免费 Chat 希望带给大家如下内容:
单例模式是一种非常简单的模式,实现的成本非常小,而从中获得收益却是巨大的。在现实生活中有些事情只能由一个人来做,比方说工作上你应该只有一个直接上级,否则你的工作将很难做;常规交通工具只有一个驾驶才能保证正确的方向……在计算机的世界里存在同样的情况。
单例最重要的特性:
一个 JVM 中只存在一个实例,伴随着应用的结束而结束。
类和实例是 Java 学习者面对的最基本概念,想想你是如何创建一个类的?最基本的操作是通过 new 关键字即可随心所欲的创建类的实例。
为了防止我们的单例被人随心所欲地创建,我们首先要做的就是禁止任何人 new 我们的单例类。
public class Singleton { //禁止任何人new你的单例 private Singleton(){};}
至此,我们已经完成了单例类变成的第一步。
紧接着你要回答一个问题,既然任何人都无法 new 你的单例类,那么他们如何获得这个类的实例呢?
一般会有三种回答:
我们来实现第一种,单例类本身应该对外提供的实例获取方法。
public class Singleton { public static Singleton getInstance(){ return new Singleton(); };}
上述代码的问题在于,每当 getInstance() 方法被调用时就会 new 一个 Singleton 实例,显然这是在糊弄自己——这只是把别人随心所欲的创建变成了自己随心所欲的创建。
问题到这里的时候你应该想到了,自己在 new Singleton 实例之前应该先判断是否已经 new 过了。对的,如果已经 new 过了就应该把当前 JVM 中的 Singleton 实例直接返回。
此时,我们显然需要一个变量来接受 new 出来的 Singleton 实例,而这个变量显然应该是 static 的,否则它无法被 getInstance() 方法引用。
于是你完整的代码变成了下面这样:
public class Singleton { static Singleton instance; public static Singleton getInstance(){ if(null ==instance) { instance = new Singleton(); } return instance; }; //禁止任何人new你的单例 private Singleton(){};}
恭喜,你已经动手写了一个单例。抓紧测试一下吧。
public class ForSingleton { public static void main(String[] args) throws Exception{ Singleton s1 = Singleton.getInstance(); Singleton s2 = Singleton.getInstance(); Singleton s3 = Singleton.getInstance(); System.out.println(s1); System.out.println(s2); System.out.println(s3); System.out.println(s1==s2); System.out.println(s2==s3); }}
控制台输出如下,这意味着无论从内存地址还是 Boolean 判断看,s1、s2、s3 都是同一个实例。
[email protected]@1b6d3586co.topc.chat.Singleton@1b6d3586truetrueProcess finished with exit code 0
请仔细想一下,我们是从什么样的出发点把代码写成这样的,这很重要。
这个出发点就是禁止任何人 new 你的单例。记住这一点我相信你随时都能写出这个单例来,直到某天有人跟你说这样的单例代码存在很大的漏洞,像个毕业生写出来的。
既然说单例的特性是在 JVM 中只存在一个实例,那么上面的测试场景似乎远远不够;我们还没有考虑在高并发、多线程的应用场景下这样的控制逻辑是否能够 hold 住压力。
理论上假设有 A、B 两个线程调用 getInstance() 方法,当线程 A 执行完 if(null ==instance)
被中断,此时线程 B 恰好也执行到此 if(null ==instance)
依然成立。所以线程 B 得到了一个 new 出来的实例,此时 CPU 让线程 A 继续执行,问题由此而发生了——线程 A 也拿到了一个 new 出来的实例。
赶紧测试一下吧:
public class MultiThreadTest implements Runnable{ @Override public void run() { Singleton s1 = Singleton.getInstance(); //理论上只要出现一次输出的内存地址不一样,就说明getInstance()返回了不同的实例 System.out.println(s1); } public static void main(String[] args) { Thread[] tArray=new Thread[1000]; /** *线程中断不是必然出现,需要尽可能多的线程来模拟或多尝试几次 */ for (int i = 0; i < 1000; i++) { Thread t1 = new Thread(new MultiThreadTest()); t1.setName("name"+i); tArray[i] = t1; } for (int i = 0; i < 1000; i++) { tArray[i].start(); } }}
看到这样的结果,意味着不想发生的事情已经发生了。
你应该心服口服别人说你的单例代码存在漏洞,像是毕业生写出来的。我猜你也会立即寻找弥补的方法,百度会教你很多种写法解决单例的线程安全问题。诸如双重检查、Synchronized 同步等。 但是请你不要看,不要看,不要看,往下面看。
当你空闲的时候随便看点源码,这里的源码指平台级的代码,如 JDK、Tomcat、Spring Framework 等。这些代码都是经过大量验证和调优的,无论是书写风格还是逻辑思维上都有很多可以学习的地方。看看 JDK 1.8 的 java.lang.Runtime:
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {}}
请认真仔细地阅读并记住上面 8 行代码,与我们开篇自己写的代码有何区别?这种源码写法是否存在线程安全问题?请在读者圈发表你的看法。
有很多百度文章都会罗列大概 8 至 9 种的单例写法,并非常清晰地表明每一种写法存在什么样的优缺点,笔者认为这很容易把人搞晕。笔者认为你没有必要去研究那些本身就是错误写法的代码,你需要知道是自己能否徒手写一段单例,并用线程安全来检验这段代码就足够了。
目前我们的应用大多是集群多实例部署,那么会有人关心单例模式在多台主机上并存时的应用场景。即每个 JVM 中都有一个实例,对于集群来说还是多实例的存在,而我们又需要保证整个集群中只有一个实例提供服务。该场景为延伸问题,笔者在应用层面的开发中也没有具体场景实际运用过。思路是这类服务类似于分部署的 master 节点,需要经过一些列状态同步选定一个节点来提供全局唯一性服务。
单例是非常简单的模式,请记住单例书写的出发点【禁止任何人 new 你的单例】和单例的注意事项【线程安全】。
本文首发于 GitChat,未经授权不得转载,转载需与 GitChat 联系。
阅读全文: http://gitbook.cn/gitchat/activity/5bf2a725186f104f04c24a72
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。