所谓单例模式,简单来说,就是在整个应用中保证只有一个类的实例存在。
作用
- 避免对象的多次创建,节约资源
单例设计模式的关键点
- 私有构造函数
- 提供方法返回单例对象
- 保证多线程情况下单例依然唯一
- 确保反序列化的时候也不会重新构建对象
上面的关键点也非必须都要坚持的点,还是需要具体场景具体分析吧。
1、最简单的实现(恶汉)
public class Singleton{
private static final Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
private Singleton(){
}
}
2、性能优化--lazy loaded(懒汉)
- 上面的代码虽然简单,但是有一个问题----无论这个类是否被使用,都会创建一个instance对象,并且这个类还不一定会被使用,那么这个创建过程就是无用的,怎么办呢?
为了解决这个问题,我们想到的新的解决方案:
public class SingletonClass {
private static SingletonClass instance = null;
public static SingletonClass getInstance() {
if(instance == null) {
instance = new SingletonClass();
}
return instance;
}
private SingletonClass() {
}
}
代码的变化有俩处----首先,把 instance 设置为 null ,知道第一次使用的时候判是否为 null 来创建对象。因为创建对象不在声明处,所以那个 final 的修饰必须去掉。
我们来想象一下这个过程。要使用 SingletonClass ,调用 getInstance()方法,第一次的时候发现instance时null,然后就创建一个对象,返回出去;第二次再使用的时候,因为这个instance事static的,共享一个对象变量的,所以instance的值已经不是null了,因此不会再创建对象,直接将其返回。
这个过程就称为lazy loaded ,也就是迟加载-----直到使用的时候才经行加载。
3、同步(应对多线程问题)
上面的代码很清楚,也很简单。单线程下,这段代码没什么问题,可是如果是多线程呢,麻烦就来了,我们来分析一下:
线程A希望使用SingletonClass,调用getInstance()方法。因为是第一次调用,A就发现instance是null的,于是它开始创建实例,就在这个时候,CPU发生时间片切换,线程B开始执行,它要使用SingletonClass,调用getInstance()方法,同样检测到instance是null——注意,这是在A检测完之后切换的,也就是说A并没有来得及创建对象——因此B开始创建。B创建完成后,切换到A继续执行,因为它已经检测完了,所以A不会再检测一遍,它会直接创建对象。这样,线程A和B各自拥有一个SingletonClass的对象——单例失败!
解决的办法也很简单,那就是加锁:
public class SingletonClass{
private static SingletonClass instance = null;
public synchronized static SingletonClass getInstance(){
if(instance == null){
instance = new SingletonClass();
}
return instance;
}
private SingletonClass(){
}
}
- 只要getInstance()加上同步锁,,一个线程必须等待另外一个线程创建完后才能使用这个方法,这就保证了单利的唯一性。
4、又是性能
上面的代码又是很清楚也很简单的,然而,往往简单的东西不够理想。这段代码毫无疑问存在性能的问题----synchronized修饰的同步块可是要比一般的代码慢上几倍的!如果存在很多次的getInstance()调用,那性能问题就不得不考虑了?!!!
让我们来分析一下,究竟是整个方法都必须加锁,还是紧紧其中某一句加锁就足够了?我们为什么要加锁呢?分析一下lazy loaded的那种情形的原因,原因就是检测null的操作和创建对象的操作分离了,导致出现只有加同步锁才能单利的唯一性。
如果这俩个操作能够原子的进行,那么单利就已经保证了。于是,我们开始修改代码:
public class SingletonClass{
private static SingletonClass instance = null;
public static SingletonClass getInstance(){
synchronized(SingletonClass.class){
if(instance == null){
instance = new SingletonClass();
}
}
return instance;
}
private SingletonClass(){
}
}
- 首先去掉 getInstance() 的操作,然后把同步锁加载到if语句上。但是,这样的修改起不到任何作用:因为每次调用getInstance()的时候必然要经行同步,性能的问题还是存在。如果............我们事先判断一下是不是为null在去同步呢?
public class SingletonClass{
private static SingletonClass instance = null;
public static SingletonClass getInstance(){
if(instance == null){
synchronized(SingletonClass.class){
if(instance == null){
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass(){
}
}
还有问题吗?首先判断instance是不是为null,如果为null在去进行同步,如果不为null,则直接返回instance对象。
这就是double---checked----locking 设计实现单利模式。到此为止,一切都很完美。我们用一种很聪明的方式实现了单例模式。
5、从源头检查
下面我们开始说编译原理。所谓编译,就是把源代码”翻译“成目标代码----大多是是指机器代码----的过程。针对Java,它的目标代码不是本地机器代码,而是虚拟机代码。编译原理里面有一个很重要的内容是编译器优化。所谓编译器优化是指,在不改变原来语义的情况下,通过调整语句顺序,来让程序运行的更快。这个过程成为reorder。
要知道,JVM只是一个标准,并不是实现。JVM中并没有规定有关编译器优化的内容,也就是说,JVM实现可以自由的进行编译器优化。
下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!
举例:
创建一个对象 new Object() 看似一句话,但是实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:
1、跟实例分配内存;
2、调用类的构造函数,初始化成员字段;
3、将instance 对象指向分配的内存空间(此时instance就不是null了);
1、2、3的顺序可能不一致,所以可能会出错。
6. 解决方案
了这么多,难道单例没有办法在Java中实现吗?其实不然!
- 在JDK 5之后,Java使用了新的内存模型。
volatile
关键字有了明确的语义,用volatile
修饰instance之后,能够保证instance对象每次都是从主内存读取的。
说明:Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
volatile
赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。这同样也导致了volatile
也会有一些性能问题,不过影响还是非常小的。
public class SingletonClass {
private volatile static SingletonClass instance = null;
public static SingletonClass getInstance() {
if (instance == null) {
synchronized (SingletonClass.class) {
if(instance == null) {
instance = new SingletonClass();
}
}
}
return instance;
}
private SingletonClass() {
}
}
这种方法的缺点
- 这只是JDK1.5之后的Java的解决方案,之前版本
volatile
还没有被赋予这个语义功能,所以这个方案不适用于之前的老版本。 - 某些情况下还是会出现失效问题,在《java并发编程实践》一书中谈到了这个问题,是不赞成这种用法的。
其实,还有另外的一种解决方案,并不会受到Java版本的影响:
静态内部类方案(Effiective Java推荐的方式)
public class SingletonClass {
private static class SingletonClassInstance {
private static final SingletonClass instance = new SingletonClass();
}
public static SingletonClass getInstance() {
return SingletonClassInstance.instance;
}
private SingletonClass() {
}
}
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为SingletonClass没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载SingletonClassInstance类,这个类有一个static的SingletonClass实例,因此需要调用SingletonClass的构造方法,然后getInstance()将把这个内部类的instance返回给使用者。由于这个instance是static的,因此并不会构造多次。
由于SingletonClassInstance是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
到这里一般就够用了,只不过如果考虑一些奇葩情况当然依然是有一些问题存在的。
1、可以反序列化创建对象。
2、可以反射调用私有构造函数创建对象。
枚举单例设计模式(最安全最简单的方式)
public enum SingleInstance {
INSTANCE;
public void test(){
System.out.println(INSTANCE.name());
}
}
调用
public class MainTest {
public static void main(String[] args) {
SingleInstance.INSTANCE.test();
}
}
写法简单是枚举单例最大的优点,枚举在java中其实编译后生成的也是一个java类,枚举不仅能够有字段,也可以有自己的方法,最重要的是枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例,在上面几种单例创建模式实现中,在反序列的情况下他们会出现重新创建对象。
我们知道序列化可以将一个单例对象写到磁盘,然后在读取回来,从而有效的获取了一个实例,即使构造函数是私有的,反序列化操作依然可以通过特殊途径去创建一个类的新的实例。
反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象反序列化。
上面几种示例如果要杜绝单例对象在反序列被重新创建的情况,就必须加入readResolve()函数,就是在这个方法里面直接将单例对象返回(复写此方法,直接返回单例对象即可),而不是新创建一个对象。(对于枚举则不会存在这个问题,枚举反序列化也不会创建新的实例【为什么:参见:枚举为什么是最好的单例,以及序列化反序列化等操作】)
另外枚举也可以避免反射创建对象(具体参见上方文章)
更多的还有容器类的单例设计管理模式,适合管理很多单例类。
容器的单例设计模式:
/**
* 单例管理类
*/
public class SingletonManger {
private static Map<String,Object> objectMap = new HashMap<>();
/**
* 私有化管理类,防止创建多个
*/
private SingletonManger(){
}
/**
* 插入单例类
* @param key
* @param instance
*/
public static void registerService(String key,Object instance){
if(!objectMap.containsKey(key)){
objectMap.put(key,instance);
}
}
/**
* 获取单例类
* @param key
* @return
*/
public static Object getSigleInstance(String key){
return objectMap.get(key);
}
}