单例模式

所谓单例模式,简单来说。就是在整个应用中保证仅仅有一个类的实例存在。就像是Java Web中的application,也就是提供了一个全局变量,用处相当广泛,比方保存全局数据,实现全局性的操作等。
 
1. 最简单的实现
 
首先,可以想到的最简单的实现是。把类的构造函数写成private的,从而保证别的类不能实例化此类。然后在类中提供一个静态的实例并可以返回给使用者。这样,使用者就行通过这个引用使用到这个类的实例了。
 
public class SingletonClass { 

  private static final SingletonClass instance = new SingletonClass(); 
    
  public static SingletonClass getInstance() { 
    return instance; 
  } 
    
  private SingletonClass() { 
     
  } 
    
}
 
如上例,外部使用者假设须要使用SingletonClass的实例,仅仅能通过getInstance()方法。而且它的构造方法是private的,这样就保证了仅仅能有一个对象存在。
 
2. 性能优化——lazy loaded
 
上面的代码尽管简单,可是有一个问题——不管这个类是否被使用,都会创建一个instance对象。假设这个创建过程非常耗时,比方须要连接10000次数据库(夸张了…:-)),而且这个类还并不一定会被使用,那么这个创建过程就是没用的。怎么办呢?
 
为了解决问题,我们想到了新的解决方式:
 
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的,所以已经不是null了。因此不会再创建对象,直接将其返回。
 
这个过程就成为lazy loaded,也就是迟载入——直到使用的时候才进行载入。
 
3. 同步
 
上面的代码非常清楚,也非常easy。然而就像那句名言:“80%的错误都是由20%代码优化引起的”。单线程下。这段代码没有什么问题。但是假设是多线程。麻烦就来了。我们来分析一下:
 
线程A希望使用SingletonClass。调用getInstance()方法。由于是第一次调用,A就发现instance是null的。于是它開始创建实例。就在这个时候。CPU发生时间片切换。线程B開始运行,它要使用SingletonClass,调用getInstance()方法。相同检測到instance是null——注意。这是在A检測完之后切换的。也就是说A并没有来得及创建对象——因此B開始创建。B创建完毕后,切换到A继续运行,由于它已经检測完了,所以A不会再检測一遍,它会直接创建对象。

这样。线程A和B各自拥有一个SingletonClass的对象——单例失败。

 
解决办法也非常easy,那就是加锁:
 
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. 又是性能
 
上面的代码又是非常清楚非常easy的,然而,简单的东西往往不够理想。

这段代码毫无疑问存在性能的问题——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构造完毕之前就是用了这个实例,程序就会出现错误了!

 
于是,我们想到了以下的代码:
 
public class SingletonClass { 

  private static SingletonClass instance = null

  public static SingletonClass getInstance() { 
    if (instance == null) { 
      SingletonClass sc; 
      synchronized (SingletonClass.class) { 
        sc = instance; 
        if (sc == null) { 
          synchronized (SingletonClass.class) { 
            if(sc == null) { 
              sc = new SingletonClass(); 
            } 
          } 
          instance = sc; 
        } 
      } 
    } 
    return instance; 
  } 

  private SingletonClass() { 

  } 
    
}
 
我们在第一个同步块里面创建一个暂时变量,然后使用这个暂时变量进行对象的创建,而且在最后把instance指针暂时变量的内存空间。写出这样的代码基于下面思想。即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对暂时变量sc进行操作并不影响instance。所以外部类在instance=sc;之前检測instance的时候。结果instance依旧是null。
 
只是。这样的想法全然是错误的!

同步块的释放保证在此之前——也就是同步块里面——的操作必须完毕。可是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器全然能够把instance=sc;这句移到内部同步块里面运行。这样。程序又是错误的了!

 
6. 解决方式
 
说了这么多,难道单例没有办法在Java中实现吗?事实上不然!

 
在JDK 5之后。Java使用了新的内存模型。

volatilekeyword有了明白的语义——在JDK1.5之前,volatile是个keyword。可是并没有明白的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!

因此,仅仅要我们简单的把instance加上volatilekeyword就能够了。

 
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的解决方式,那之前版本号呢?事实上,还有另外的一种解决方式,并不会受到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()也并不须要加同步。

至此。我们完整的了解了单例模式在Java语言中的时候。提出了两种解决方式。个人偏向于另外一种。而且Effiective Java也推荐的这样的方式。
 
posted on 2016-02-23 14:47  gcczhongduan  阅读(153)  评论(0编辑  收藏  举报