用我自己惨痛的经历说一下优快云的这个Markdown编辑器。现在是我第三遍码这篇博客,之前电脑还没配环境写了一个开头扔草稿箱了,昨晚开始继续码,码了两遍。第一遍不小心敲到了F5,界面就只剩下原来存在草稿箱里面的内容了,不甘心,码第二遍,写到最后准备挂上Github地址,一下忘记怎么给文字加颜色了,打开以前的一篇博客,又启开一个MarkDown,再切回来就傻逼了,当前页面正在试图关闭窗口,是,否,我选择了否,然后就是这样一张图,
图后面的文字是我之前写的,所以为了演示这个又码了一遍开头。点击重新加载之后就有Game Over了。“Markdown编辑器已经停止,因为另一个实例中相同的浏览器中运行”,好像有点病句,我们把一个浏览器看成一个系统,显然在这个系统中,Markdown编辑器是单例模式的,只允许存在唯一一个实例,这就是今天要说的单例模式。
单例模式应该算是最最简单的一种设计模式了,也是应用很广的一种设计模式。顾名思义,在系统中单例模式的对象必须保证只有一个实例存在,比如一个Android程序中只存在一个Application对象,或者说一个进程中只存在一个Application对象。还有InputMethodManager,日历,短信模块,开源项目ImageLoader等等,在Android中有很多单例模式的具体应用。那么我们为什么要去使用单例模式呢,Java中创建对象和销毁对象都是很耗资源的,对于一些创建对象需要消耗很多资源的场景,比如ImageLoader,涉及到线程池,缓存,网络请求等,我们没有必要频繁的去创建对象,只需要向整个系统提供一个实例就可以了。实现单例模式比较简单,但是有一些需要注意的地方。书中总结了一下几个关键点:
(1)一般使用私有的构造函数,使外界不能通过new来创建对象。
(2)通过一个静态方法或者枚举返回单例类对象。
(3)确保单例类的对象只有一个,尤其是在多线程情况下,考虑线程安全问题。
(4)确保单例类在反序列化时不会重新创建对象。
下面介绍几种典型的单例模式的实现形式。
1.饿汉模式
public class Singleton {
public static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
饿汉模式的实现方式很简单,在加载类的时候就已经实例化了该类的一个静态对象,天生就是线程安全的。但是没有考虑延时加载,什么是延时加载呢,也叫懒加载,就是在需要的时候再去实例化对象,比如说在调用getInstance方法的时候。这样就可能造成某些情况下的不必要的资源消耗。
2.懒汉模式
public class Singleton {
private Singleton(){}
private static Singleton instance=null;
public static synchronized Singleton getInstance(){
if (instance==null){
instance=new Singleton();
}
return instance;
}
}
懒汉模式弥补了饿汉模式的缺点,使用了在延时加载,加载类的时候不会去实例化对象,第一次调用getInstance方法的时候才去初始化instance对象。但是这么做带来的高并发情况下的线程安全问题是不可避免的,难以保证系统中实例是唯一存在的,违背了单例模式的原则。这里使用了synchronized关键字,看起来解决了线程安全的问题,但是每次调用getInstance方法都去进行同步操作,这样带来了很多不必要的同步开销,毕竟大多数情况下我们是不需要进行同步操作的。所以,这种模式的单例模式是不推荐使用的。
3.Double CheckLock模式
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Double CheckLock模式,听着名字挺高大上,取个中文名叫双检查锁模式,其实看看代码还是很好理解的,可以看成是懒汉模式的加强版。这里同样使用了延时加载,调用getInstance方法的时候才会去实例化对象,getInstance方法是这里的重点。里面对instance对象进行了两层非空的检查,第一层判断当instance非空时不会去进行同步操作,避免了不必要的消耗,第二层判断是为了在instance为空时进行实例化对象。看起来一切都是这么美好,延时加载,线程安全,同时避免了不必要的同步操作。但是,由于Java内存模型的原因,在高并发环境下偶尔会出现失效问题,具体原因就不在这里阐述了,有兴趣的可以看看Android源码设计模式29页的解释(码第三遍,页数都记住了。。)。
4.静态内部类模式
public class Singleton {
private Singleton(){}
private static class InstanceHolder{
private static final Singleton instance=new Singleton();
}
public static Singleton getInstance(){
return InstanceHolder.instance;
}
}
静态内部类单例模式是比较推荐的一种方式。当第一次调用getInstance方法,加载静态内部类InstanceHolder的时候才去实例化对象,延时加载。同时也是线程安全的,保证了实例在系统中的唯一存在。
5.枚举模式
public enum Singleton{
INSTANCE
}
枚举模式的有点,够简单粗暴,而且枚举的创建天生就是线程安全的。搜索了一下,很多人都是极力推荐这种方法,但是总是看起来有点怪怪的,可能是不怎么经常使用枚举的原因吧,看不到一点类的味道。
最后总结一下,不管是哪种模式,要注意的都是一下几点:
(1)私有的构造方法
(2)提供一个静态方法向外界提供实例
(3)保证在系统中实例是唯一存在的
(4)注意在高并发环境下的线程安全问题
(5)使用延时加载
示例代码托管地址:https://github.com/lulululbj/JavaDesignPatterns