7种Java单例模式

单例模式 - 终极篇

1.  前言

单例(Singleton)是设计模式当中使用比较常用和重要的一种模式,有些架构师并不把单例作为一种设计模式,而是一种实现方式。下面是我自己总结的7中单例模式的写法,废话不多说,直接上代码:(分享注明出处即可,看完这一篇基本上不用再看其他乱起八糟的总结了!)

2. 什么是单例?

单例对象的类必须保证只有一个实例存在(from wiki

懒汉式:lazy load

第一种(懒汉式简单版):

public class Single1 {
	
	private static Single1 instance;//此处一定是私有
//	public Single1(){}  省略默认构造
	public static Single1 getInstance(){
		if (instance==null) {
			instance=new Single1();
		}
		return instance;
	}
}

很明显这是一种有缺陷的写法,初步优化,是将构造器私有,这样可以防止被外部的类调用。

1.	//ViViD  
2.	public class Singleton {  
3.	    private static Singleton instance = null;  
4.	  
5.	    private Singleton() {  
6.	    }  
7.	  
8.	    public static Singleton getInstance() {  
9.	        if (instance == null) {  
10.	            instance = new Singleton();  
11.	        }  
12.	        return instance;  
13.	    }  
14.	}  

这种写法在大多数的时候也是没问题的。虽然具备lazyloding,但致命缺点:多线程下不能正常工作。这种写法能够在多线程中很好的工作,但是,遗憾的是,效率很低,99%情况下不需要同步。问题在于,当多线程工作的时候,如果有多个线程同时运行到if (instance == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。

第二种(懒汉,线程安全):

1.	public class Singleton {  
2.	    private static Singleton instance = null;  
3.	  
4.	    private Singleton() {  
5.	    }  
6.	  
7.	    public static Synchronized Singleton getInstance() {  
8.	        if (instance == null) {  
9.	            instance = new Singleton();  
10.	        }  
11.	        return instance;  
12.	    }  
13.	}

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

加上synchronized关键字之后,getInstance方法就会锁上了。如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这端代码也就避免了第二种中,可能出现因为多线程导致多个实例的情况。
但是,这种写法也有一个问题:给gitInstance方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。

第三种( Double-Check双重检验锁 )

Version2代码相对于Version1d代码的效率问题,其实是为了解决1%几率的问题,而使用了一个100%出现的防护盾。那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。 JDK1.5 以后源码里就是这样写的。
——有没有这样的方法呢?当然是有的,改进后的代码Vsersion3如下:

1.	// 带双重检测锁的单例  
2.	public class Singleton {  
3.	    private static Singleton5 instance = null;  
4.	  
5.	    private Singleton() {  
6.	    }  
7.	  
8.	    public static Singleton getInstance() {  
9.	        if (instance == null) {  
10.	            synchronized (Singleton.class) {  
11.	                if (instance == null) {  
12.	                    instance = new Singleton();  
13.	                }  
14.	            }  
15.	        }  
16.	        return instance;  
17.	    }  
18.	}  

这个是第二种方式的升级版,俗称双重检查锁定,详细介绍请查看:JDK源码。

在JDK1.5之后,双重检查锁定才能够正常达到单例效果。

这个版本的代码看起来有点复杂,注意其中有两次if (instance == null)的判断,这个叫做『双重检查Double-Check』。

·        第一个if (instance == null),其实是为了解决Version2中的效率问题,只有instance为null的时候,才进入synchronized的代码段——大大减少了几率。

·        第二个if (instance == null),则是跟Version2一样,是为了防止可能出现多个实例的情况。

—— 这段代码看起来已经很完美了。
—— 当然,只是『看起来』,还是有小概率出现问题的。
这弄清楚为什么这里可能出现问题,首先,我们需要弄清楚几个概念:原子操作、指令重排。

饿汉式:eagerly load

饿汉式单例是指:指全局的单例实例在类装载时构建的实现方式。由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。

第四种(饿汉,线程不安全):

1.	public class Singleton {  
2.	    //2、自定义一个本类对象。  
3.	    private static Singleton1 intance = new Singleton();  
4.	    //1、私有化构造函数。  
5.	    private Singleton() {  
6.	    }  
7.	    //3、定义一个方法返回改对象。让其他程序通过这个方法就可以获取该对象。  
8.	    public static Singleton getInstance() {  
9.	        return intance;  
10.	    }  
11.	}  

第五种(静态内部类):

1.	public class Singleton5 {  
2.	    private Singleton5() {}  
3.	  
4.	    private static class SingletonHolder {  
5.	        private static final Singleton5 INSTANCE = new Singleton7();  
6.	    }  
7.	  
8.	    public static final Singleton5 getInstance() {  
9.	        return SingletonHolder.INSTANCE;  
10.	    }  
11.	}  

这种方式同样利用了 classloder 的机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了, instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance 。想象一下,如果实例化 instance 很消耗资源,我想让他延迟加载,另外一方面,我不希望在 Singleton 类加载时就实例化,因为我不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

第六种(枚举优雅版):

1.	public enum Singleton {    
2.	    INSTANCE;    
3.	    public void whateverMethod() {    
4.	    }   
5.	}    

这是一个枚举类型……连class都不用了,极简。使用时可以直接Singleton.INSTANCE. whateverMethod();由于创建枚举实例的过程是线程安全的,所以这种写法也没有同步的问题。

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。个人认为由于1.5中才加入enum特性,普及率不够高,在实际工作中,发现被使用的不是很广泛。

作者对这个方法的评价:

这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

枚举单例这种方法问世一些,许多分析文章都称它是实现单例的最完美方法——写法超级简单,而且又能解决大部分的问题。
不过我个人认为这种方法虽然很优秀,但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。

第七种终极版 (volatile)

对于Double-Check这种可能出现的问题(当然这种概率已经非常小了,但毕竟还是有的嘛~),解决方案是:只需要给instance的声明加上volatile关键字即可,volatile版本如下:

1.	public class Singleton  
2.	{  
3.	    private volatile static Singleton singleton = null;  
4.	    private Singleton()  {    }  
5.	    public static Singleton getInstance()   {  
6.	        if (singleton== null)  {  
7.	            synchronized (Singleton.class) {  
8.	                if (singleton== null)  {  
9.	                    singleton= new Singleton();  
10.	                }  
11.	            }  
12.	        }  
13.	        return singleton;  
14.	    }  
15.	} 

   volatile关键字的一个作用是禁止指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。

    注意:volatile阻止的不singleton = newSingleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。

  ——也就彻底防止了Version3中的问题发生。
——好了,现在彻底没什么问题了吧?
……
……
……
好了,别紧张,的确没问题了。大名鼎鼎的EventBus中,其入口方法EventBus.getDefault()就是用这种方法来实现的



  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值