作者 : 夏至 欢迎转载,也请保留这段申明
http://blog.csdn.net/u011418943/article/details/60139644
上一篇讲到策略模式,变动的代码需要用到策略模式,感兴趣的小伙伴可以看看.
传送门:Android 常用设计模式之 -- 策略模式
单例模式的定义就不解释过多了,相信很多小伙伴在设计的时候,都用到这个模式;常用的场景为 数据库的访问,文件流的访问以及网络连接池的访问等等,在这些场景中,我们都希望实例只有一个,除了减少内存开销之外,也防止防止多进程修改文件错乱和数据库锁住的问题。
在这一篇文章中,我将带你分析 android 常见的集中单例模式,并详细分析他们的优缺点。让大家在以后的选择单例中,可以根据实际情况选择。
当然,如有错误,也欢迎指正。
下面是介绍:
1、饿汉式
就是初始化的时候,直接初始化,典型的以时间换空间的做法。
public class SingleMode{
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
//当类被初始化的时候,就直接new出来
private static SingleMode instance = new SingleMode();
//提供一个方法,给他人调用
public static SingleMode getInstance(){
return instance;
}
}
饿汉式的好处是线程安全,因为虚拟机保证只会装载一次,再装载类的时候,是不会并发的,这样就保证了线程安全的问题。
但缺点也很明显,一初始化就实例占内存了,但我裤子还没脱,不想用呢。
2、懒汉式
为了解决上面的问题,开了懒汉式,就是需要使用的时候,才去加载;
public class SingleMode{
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
private static SingleMode mSingleMode;
public static SingleModegetInstance(){ //这里就是延时加载的意思
if (mSingleMode == null){
mSingleMode = new SingleMode();
}
return mSingleMode;
}
}
懒汉式如上所示,
优点:
我们只需要在用到的时候,才申请内存,且可以从外部获取参数再实例化,这点是懒汉式的最大优点了
缺点:
单线程只实例了一次,如果是多线程了,那么它会被多次实例
至于问什么说它是线程不安全的呢?先下面这张图:
我们假设一下,有两个线程,A和B都要初始化这个实例;此时 A 比较快,已经判断 mSingleMode 为null,正在创建实例,而 B 这时候也再判断,但此时 A 还没有 new 完,所以 mSingleMode 还是为空的,所以B 也开始 new 出一个对象出来,这样就相当于创建了两个实例了,所以,上面这种设计并不能保证线程安全。
2.1、如何实现懒汉式线程安全?
有人会说,简单啊,你既然是线程并发不安全,那么加上一个 synchronized 线程锁不就完事了?但是这样以来,会降低整个访问速度,而且每次都要判断,这个真的是我们想要的吗?
由于上面的缺点,所以,我们可以对上面的懒汉式加个优化,如双重检查枷锁:
public class SingleMode{
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
private static SingleMode mSingleMode;
public static SingleMode getInstance(){
if (mSingleMode == null){
synchronized (SingleMode.class){
if (mSingleMode == null){ //二次检测
mSingleMode = new SingleMode();
}
}
}
return mSingleMode;
}
}
在上面的基础上,用了二次检查,这样就保证了线程安全了,它会先判断是否为null,是才会去加载,而且用 synchronized 修饰,则又保证了线程安全。
但是如果上面我们没有用 volatile 修饰,它还是不安全的,有可能会出现null的问题。为什么?这是因为 java 在 new 一个对象的时候,它是无序的。而这个过程我们假设一下,假如有线程A,判断为null了,这个时候它就进入线程锁了 mSingleMode = new SingleMode();,它不是一蹴而就,而是需要3步来完成的。
- 1、为 mSingleMode 创建内存
- 2、new SingleMode() 调用这个构造方法
- 3、mSingleMode 指向内存区域
那你可能会有疑问,这样不是很正常吗?怎么会有 null 的情况?
非也,java 虚拟机在执行上面这三步的时候,并不是按照这样的顺序来的,可能会打乱,这儿就是java重排序,比如2和3调换一下:
- 1、为 mSingleMode 创建内存
- 3、mSingleMode 指向内存区域
- 2、new SingleMode() 调用这个构造方法
那这个时候,mSingleMode 已经指向内存区域了,那这个时候它就不为 null了,而实际上它并未获得构造方法,比如构造方面里面有些参数或者方法,但是你并未获取,然而这个时候线程B过来,而 mSingleMode已经指向内存区域不为空了,但方法和参数并未获得, 所以,这样你线程B在执行 mSingleMode 的某些方法时就会报错。
当然这种情况是非常少见的,不过还是暴露了这种问题所在。
所以我们用volatile 修饰,我们都知道 volatile 的一个重要属性是可见性,即被 volatile 修饰的对象,在不同线程中是可以实时更新的,也是说线程A修改了某个被volatile修饰的值,那么我线程B也知道它被修改了。但它还有另一个作用就是禁止java重排序的作用,这样我们就不用担心出现上面这种null 的情况了。如下:
public class SingleMode{
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
private volatile static SingleMode mSingleMode;
public static SingleMode getInstance(){
if (mSingleMode == null){
synchronized (SingleMode.class){
if (mSingleMode == null){ //二次检测
mSingleMode = new SingleMode();
}
}
}
return mSingleMode;
}
}
看到这里,是不是感觉爬了几百盘的坑,终于上了黄金段位了。。。
然而,并不是,你打了排位之后发现还是被吊打,所以我们可能还忽略了什么。
没错,这种方式,依旧存在缺点:
由于volatile关键字会屏蔽会虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此也建议,没有特别的需要,不要大量使用。
笔者就遇到,使用这种模式,不知道什么原因,第二次进入 activity的时候,view 刷不出来,然而数据对象什么的都存在,调得我心力交瘁,欲生欲死,最后换了其他单例模式就ok了,希望懂的大侠告诉我一下,我只能怀疑volatile了。。。。。。
那你都这样说了,那还怎么玩,有没有一种更好的方式呢?别急,往下看。
3、静态式
什么叫静态式呢?回顾一下上面的饿汉式,我们再刚开始的就初始化了,不管你需不需要,而我们也说过,Java 再装载类的时候,是不会并发的,那么,我们能不能zuo做到懒加载,即需要的时候再去初始化,又能保证线程安全呢?当然可以,如下:
public class SingleMode{
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
public static class Holder{
private static SingleMode mSingleMode = new SingleMode();
public static SingleMode getInstance(){
return mSingleMode;
}
}
}
除了上面的饿汉式和懒汉式,,静态的好处在于能保证线程安全,不用去考虑太多、缺点就在于对参数的传递比较不好。
那么这个时候,问题来了,参数怎么传递?这个确实没懒汉式方便,不过没关系,我们可以定义一个init()就可以了,只不过初始化的时候多了一行代码;如:
public class SingleMode {
//构造方法私有化,这样外界就不能访问了
private SingleMode(){
};
public static class Holder{
private static SingleMode mSingleMode = new SingleMode();
public static SingleMode getInstance(){
return mSingleMode;
}
}
private Context mContext;
public void init(Context context){
this.mContext = context;
}
}
初始化:
SingleMode mSingleMode = SingleMode.Holder.getInstance();
mSingleMode.init(this);
4、枚举单例
java 1.4 之前,我们习惯用静态内部类的方式来实现单例模式,但在1.5之后,在 《Effective java》也提到了这个观点,使用枚举的优点如下:
- 线程安全
- 延时加载
- 序列化和反序列化安全
所以,现在一般用单个枚举的方式来实现单例,如上面,我们改一下:
public static SingleMode getInstance(){
return Singleton.SINGLETON.getSingleTon();
}
public enum Singleton{
SINGLETON ; //枚举本身序列化之后返回的实例,名字随便取
private AppUninstallModel singleton;
Singleton(){ //JVM保证只实例一次
singleton = new AppUninstallModel();
}
// 公布对外方法
public SingleMode getSingleTon(){
return singleton;
}
}
好吧,这样就ok了,但还是那个问题,初始化参数跟静态类一样,还是得重新写个 init() 有失必有得吧。
这样,我们的单例模式就学完了。