单例模式是一种简单常用的设计模式。单例模式可以保证系统中有且只有一个该对象的实例,主要分为懒汉式和饿汉式。单例模式虽然是设计模式中比较初级的模式,但是经过此次深入研究之后你会发现这种设计模式并不“初级”。往往最简单的东西在不同的审视角度下会呈现多面性,这种多面性导致事物的复杂性。
1.饿汉式
饿汉式顾名思义,就是比较“饿”的一种,它主要“饿”在没有实例化静态变量。
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
2.懒汉式
懒汉式顾名思义,就是比较“懒”的一种,它主要“懒”在事先将静态变量实例化。
public class Singleton{
private static final Singleton instance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
3.比较
饿汉式和懒汉式的区别在于是否初始化静态自身变量。
懒汉式在类加载的时候就已经初始化实例,在代码运行前就已经确定自己的唯一性。这样的好处在于不会出现并发问题,毕竟多线程的资源竞争读写并发问题主要出现在代码运行期间,但是懒汉式在类加载的时候就将这种风险屏蔽掉。但是这样做也有很多不合理,譬如有些场景需要延迟加载这个类(在需要的时候才初始化),如果这时候使用懒汉式会不符合应用场景;有些场景的初始类需要做大量计算并且这些类还是不一定会用到,如果这时候在性能角度上面懒汉式又落了下风。
饿汉式是在类调用的时候判断是否为空来确定是否初次调用,如果初次调用则实例化自身赋值给静态变量,之后再此取这个实例的时候会返回之前的实例过的变量。这样的饿汉式在单线程下是没有问题的,但是在多线程的情况下会出现并发问题。例如:线程a获取实例x(x使用上面的饿汉式),当程序刚刚运行过if(instance==null)的地方,时间片发生切换,这时时间片交给了线程b,这时线程b也来获取实例x,这时候instance还是null,这样线程b获取到了一个重新new到的x,然后时间片再次切换给a,a继续执行也获取到一个重新new到的x,这样两个线程获得到了两个不同的x的实例,这样违反了单例模式的意义(程序中只有一个实例)。这样我们需要引入线程安全的单例模式。
4.线程安全的单例模式
public class Singleton(){
private static Singleton instance = null;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(null==instance){
instance = new Singleton();
}
return instance;
}
}
上面的代码基本实现了线程安全,但是这样实现会有一些效率问题,因为修饰词在方法上面,只要同时调用方法都会发生阻塞。而我们在instance在不是null的时候不会发生并发问题,所以我们浪费了很多的性能,我们要优化下代码。
public class Singleton(){
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(null!=instance){
synchronized(Singleton.class){
instance = new Singleton();
}
}
return instance;
}
}
上面的代码虽然效率提高了,但是并发的问题回来了~~~,在边界处虽然阻塞了,但是还是会发生并发问题,所以一些聪明提出了一种解决办法,双重锁检查(double-check lock,DCL)来解决上面的问题。
public class Singleton(){
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(null!=instance){
synchronized(Singleton.class){
if(null!=instance){
instance = new Singleton();
}
}
}
return instance;
}
}
这样虽然看起来没问题了,但是用句javaworld相应文章标题来说就是smart,but broken。这里存在一个指令重排乱序执行的问题,指令重排乱序执行指的是cpu允许将多条指令不按照程序规定的顺序分开发送给各相应电路单元处理的技术,这样就导致了可能越过边界执行代码的问题。乱序执行在这里就不详细介绍,这里面东西太多了(内存屏障,多处理器等)。
5.volatile解决办法
在jdk1.5之后出现了关键字volatile,使用volatile可以解决乱序执行问题。
public class Singleton(){
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(null!=instance){
synchronized(Singleton.class){
if(null!=instance){
instance = new Singleton();
}
}
}
return instance;
}
}
6.总结
由此可见一个小小的单例模式会出现如此多的问题,所以我们对任何事情都要做到深挖,理解它吃透它。