谈到设计模式,相信很多人脑子里蹦出的第一个就是单例模式。好的,如你所愿,今天设计模式的第一篇,我们从单例模式开始谈起。
使用场景
在一个系统中,要求一个类有且一个对象,如果出现多个对象会带来不必要的麻烦,可以采用单例模式。具体的场景有:
要求生成唯一序列号的环境。
在整个项目中需要一个共享访问点或共享数据,如计数器。
创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
需要定义大量的静态常亮和静态方法,如工具类。
一 饿汉式
首先是最简单的实现方式,饿汉式。
为了保证类只能创建出一个对象,我们首先需要将构造方法设置为私有,无法为外界所调用。接着在类加载(应用启动)时就初始化一个对象出来。最后提供一个静态公共方法,获取这个对象。
1 | public class Singleton{ |
二 懒汉式
基础版
上面的饿汉式简单粗暴,但有一个缺点,就是在类加载时就需要初始化这个对象。如果单例占用内存很少,初始化速度很快,这样使用没什么问题。但是如果单例占用的内存比较大,而应用对启动速度又有要求,我们就需要对上面的方式加以优化了。
1 | public class Singleton{ |
懒汉式顾名思义,只有在需要的时候,才进行单例的初始化操作。首次调用会创建出新的对象,再次调用会直接返回之前已创建出来的对象。
同步锁
乍一看上去,上面基础版的写法完美解决了问题。但细看看,这样写在多线程时,可能会带来新的问题。如果现在有两个线程同时在执行getInstance()方法,线程A刚进行了判断,还没来得及创建对象,这时线程B也执行到了判断,会发现instance仍然还是为null。这个时候所谓的单例就名不符实了,因为创建出了两个对象。而饿汉式不会出现这个问题,是因为JVM只会加载一次。
熟悉点并发知识会很容易解决这个问题。既然getInstance()是线程不安全的,那么我们给它加上一个同步锁就好了。
1 | public class Singleton{ |
双重校验锁
上面的同步锁写法在逻辑上已经没什么漏洞了,但仍然有优化的空间。因为这样写,每次调用getInstance()方法时都会受到同步锁synchronized的影响。而事实上,这个同步锁是对性能有消耗的。如果instance已经实例化了,我们就不需要再获取同步锁了,从而提高性能。
首先把synchronized从方法声明中移到方法体中,这样的效果与上面的完全一致。
1 | public class Singleton{ |
接着在synchronized外面再加一层判断,只有在没有初始化时,才会获取同步锁。
1 | public class Singleton { |
三 静态内部类
因为JVM在进行类加载的时候会保证数据同步的,所以我们可以采用静态内部类来保证线程安全。
1 | public class Singleton{ |
这样写十分简洁,而且同时解决了线程安全和延迟加载的问题。
四 枚举
《Effective Java》中推荐了一种更简洁的方式来实现单例,它就是枚举。
创建枚举实例是线程安全的,所以不需要额外做并发处理。
1 | public enum Singleton{ |
调用方法时可以直接这么用。
1 | Singleton.INSTANCE.functionA(); |
要注意的是,在Android中并不十分推荐使用这种方法。因为它虽然简洁,但枚举内存占用是静态变量的两倍以上,所以尽可能地去避免使用枚举。
总结
好了,设计模式的第一篇,单例模式差不多就讲到这里了。最后再归纳一下单例模式的优缺点。
优点
单例模式在内存中只有一个实例,减少了内存开支。当一个对象需要频繁地创建、销毁,或者对象的产生需要比较多的资源时,单例的优势就非常明显。
单例模式可以避免对资源的多重占用。比如一个写文件操作,由于只有一个实例,可以避免对资源文件的同时操作。
单例模式可以在系统设置全局的访问点,优化和共享资源访问。比如负责所有数据表的映射处理的单例类。
缺点
单例模式一般没有接口(因为它要求自行实例化,接口对它没有意义),所以扩展很困难。
单例模式如果持有Context,很容易引发内存泄漏。要注意传递给单例对象的Context最好是Application Context。
单例模式与单一职责原则有冲突。
单例模式:确保一个类仅有一个实例,而且自行实例化并向整个系统提供这个实例。