java设计模式之--单例模式
单例模式
单例模式限度类的实例和确保java类在java虚拟机中只有一个实例的存在。
单例类必须提供一个全局的拜访来获取类的实例。
单例模式用来日志,驱动对象,缓存和线程池。
单例设计模式也用在其余设计模式,例如形象工厂,建造者,原型,门面等设计模式。
单例模式还用在外围java中,例如java.lang.Runtime, java.awt.Desktop
java单例模式
为了实现Singleton模式,咱们有不同的办法,但它们都有以下独特的概念。
- 公有构造方法限度从其余类初始化类的实例。
- 公有动态变量与该类的实例雷同。
- 私有静态方法返回类的实例,这是提供给内部拜访的全局拜访点来获取单例类的实例。在以下的章节,咱们将学习单例模式的不同实现办法。
常见的实现形式如下
- 饿汉式
- 懒汉式
volatile双重查看锁机制
- 动态外部类
- 枚举(天生单例)
饿汉式
顾名思义,饿汉式就是第一次援用该类的时候就创立实例对象,而不论是否须要。代码如下:
public class Singleton { private static Singleton singleton = new Singleton(); private Singleton() {} public static Singleton getSignleton(){ return singleton; } }
优缺点:这样做的益处是代码简略,然而无奈做到提早加载。然而很多时候咱们心愿可能提早加载,从而减小负载,所以就有了上面的懒汉式;
懒汉式
单线程写法
这种写法是最简略的,由公有结构器和一个私有动态工厂办法形成,在工厂办法中对singleton进行null判断,如果是null就new一个进去,最初返回singleton对象。
这种办法能够实现延时加载,然而有一个致命弱点:线程不平安。如果有两条线程同时调用getSingleton()办法,就有很大可能导致反复创建对象。
public class Singleton { private static Singleton singleton = null; private Singleton(){} public static Singleton getSingleton() { if(singleton == null) { singleton = new Singleton(); } return singleton; }}
线程平安写法
这种写法思考了线程平安,将对singleton的null判断以及new的局部应用synchronized
进行加锁。同时,对singleton对象应用volatile
关键字进行限度,保障其对所有线程的可见性,并且禁止对其进行指令重排序优化
。如此即可从语义上保障这种单例模式写法是线程平安的。留神,这里说的是语义上,理论应用中还是存在小坑的,会在后文写到。
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } return singleton; } }
双重查看锁
尽管下面这种写法是能够正确运行的,然而其效率低下,还是无奈理论利用。因为每次调用getSingleton()办法,都必须在synchronized这里进行排队,而真正遇到须要new的状况是非常少的。所以,就诞生了第三种写法:
public class Singleton { private static volatile Singleton singleton = null; private Singleton(){} public static Singleton getSingleton(){ if(singleton == null){ synchronized (Singleton.class){ if(singleton == null){ singleton = new Singleton(); } } } return singleton; } }
这种写法被称为“双重查看锁”,顾名思义,就是在getSingleton()办法中,进行两次null查看。看似多此一举,但实际上却极大晋升了并发度,进而晋升了性能。为什么能够进步并发度呢?就像上文说的,在单例中new的状况非常少,绝大多数都是能够并行的读操作。因而在加锁前多进行一次null查看就能够缩小绝大多数的加锁操作,执行效率进步的目标也就达到了;
双重查看锁机制的坑
那么,这种写法是不是相对平安呢?后面说了,从语义角度来看,并没有什么问题。然而其实还是有坑。
- 说这个坑之前咱们要先来看看volatile这个关键字。其实这个关键字有两层语义。
- 第一层语义大家绝对比拟相熟,可见性。可见性是指在一个线程中对该变量的批改由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反馈到其余线程的读写操作中。顺便一提,工作内存和主内存能够近似了解成电脑中的高速缓存和主存,
工作内存是线程独享的,主存是线程共享的
。 - volatile的第二层语义是避免指令重排。大家晓得咱们写的代码(尤其是多线程代码),因为编译器优化,在理论执行的时候可能和咱们编写的程序不同。编译器只保障程序执行后果和源代码雷同,却不保障理论指令的程序和源代码雷同。这在单线程没什么问题,然而一旦引入多线程,这种乱序就可能导致重大问题。volatile关键字就能够从语义上解决这个问题。
留神,禁止指令重排优化这条语义直到jdk1.5当前能力正确工作。此前的JDK中即便将变量申明为volatile也无奈完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重查看锁模式的单例模式是无奈保障线程平安的。
动态外部类
那么,有没有一种延时加载,并且能保障线程平安的简略写法呢?咱们能够把Singleton实例放到一个动态外部类中,这样就防止了动态实例在Singleton类加载的时候就创建对象,并且因为动态外部类只会被加载一次,所以这种写法也是线程平安的:
public class Singleton { private static class Holder { private static Singleton singleton = new Singleton(); } private Singleton(){} public static Singleton getSingleton(){ return Holder.singleton; }}
然而,下面提到的所有实现形式都有两个独特的毛病:
- 都须要额定的工作(
Serializable、transient、readResolve()
)来实现序列化,否则每次反序列化一个序列化的对象实例时都会创立一个新的实例。 - 可能会有人应用反射强行调用咱们的公有结构器(如果要防止这种状况,能够批改结构器,让它在创立第二个实例的时候抛异样)。
枚举写法
当然,还有一种更加优雅的办法来实现单例模式,那就是枚举写法:
public enum SingleEnum { NEW_INSTANCE { @Override protected void doSomething() { System.out.println("----业务办法调用----"); } }; SingleEnum() { } /** * 业务办法定义 */ protected abstract void doSomething(); public static void main(String[] args) { SingleEnum.NEW_INSTANCE.doSomething(); }}
应用枚举除了线程平安和避免反射强行调用结构器之外,还提供了主动序列化机制,避免反序列化的时候创立新的对象。因而,Effective Java举荐尽可能地应用枚举来实现单例。
总结
代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是jdk版本)下,天然有不同的最优解(或者说较优解)。
比方枚举,尽管Effective Java中举荐应用,然而在Android平台上却是不被举荐的。在这篇Android Training中明确指出:
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
再比方双重查看锁法,不能在jdk1.5之前应用,而在Android平台上应用就比拟释怀了(个别Android都是jdk1.6以上了,不仅修改了volatile的语义问题,还退出了不少锁优化,使得多线程同步的开销升高不少)。
最初,不论采取何种计划,请时刻牢记单例的三大要点:
- 线程平安
- 提早加载
- 序列化与反序列化平安
参考资料
《Effective Java(第二版)》
《深刻了解Java虚拟机——JVM高级个性与最佳实际(第二版)》