單例模式中的各種問題

Singleton模式就爲我們提供了這樣實現的可能。使用Singleton的好處還在於可以節省內存,因爲它限制了
實例的個數,有利於Java垃圾回收(garbage collection)。

使用Singleton注意事項:
有時在某些情況下,使用Singleton並不能達到Singleton的目的,如有多個Singleton對象同時被不同的類
裝入器裝載;在EJB這樣的分佈式系統中使用也要注意這種情況,因爲EJB是跨服務器,跨JVM的

單態模式的演化:
單態模式是個簡單的模式,但是這個簡單的模式也有很多複雜的東西。


(注意:在這裏補充一下,現在單態模式其實有一個寫法是不錯的見這裏:http://www.blogjava.net/dreamstone/archive/2007/02/27/101000.html,但還是建議看完這篇文章,因爲解釋的事情是不一樣的,這裏說的是爲什麼double-checked不能使用.)
一,首先最簡單的單態模式,單態模式1
import java.util.*;
class Singleton
{
  private static Singleton instance;
  private Vector v;
  private boolean inUse;

  private Singleton()
  {
    v = new Vector();
    v.addElement(new Object());
    inUse = true;
  }

  public static Singleton getInstance()
  {
    if (instance == null)          //1
      instance = new Singleton();  //2
    return instance;               //3
  }
}
這個單態模式是不安全的,爲什麼說呢 ?因爲沒考慮多線程,如下情況
Thread 1 調用getInstance() 方法,並且判斷instance是null,然後進入if模塊,
在實例化instance之前,
Thread 2搶佔了Thread 1的cpu
Thread 2 調用getInstance() 方法,並且判斷instance是null,然後進入if模塊,
Thread 2 實例化instance 完成,返回
Thread 1 再次實例化instance
這個單態已經不在是單態

二,爲了解決剛纔的問題:單態模式2
public static synchronized Singleton getInstance()
{
  if (instance == null)          //1
    instance = new Singleton();  //2
  return instance;               //3
}
採用同步來解決,這種方式解決了問題,但是仔細分析
正常的情況下只有第一次時候,進入對象的實例化,須要同步,
其它時候都是直接返回已經實例化好的instance不須要同步,
大家都知到在一個多線程的程序中,如果同步的消耗是很大的,很容易造成瓶頸

三,爲了解決上邊的問題:單態模式3,加入同步
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {
      instance = new Singleton();
    }
  }
  return instance;
}
同步改成塊同步,而不使用函數同步,但是仔細分析,
又回到了模式一的狀態,再多線程的時候根本沒有解決問題

四,爲了對應上邊的問題:單態模式4,也就是很多人採用的Double-checked locking
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {  //1
      if (instance == null)          //2
        instance = new Singleton();  //3
    }
  }
  return instance;
}
這樣,模式一中提到的問題解決了。不會出現多次實例化的現象
當第一次進入的時候,保正實例化時候的單態,在實例化後,多線程訪問的時候直接返回,不須要進入同步模塊,
既實現了單態,又沒有損失性能。表面上看我們的問題解決了,但是再仔細分析:
我們來假象這中情況:
Thread 1 :進入到//3位置,執行new Singleton(),但是在構造函數剛剛開始的時候被Thread2搶佔cpu
Thread 2 :進入getInstance(),判斷instance不等於null,返回instance,
(instance已經被new,已經分配了內存空間,但是沒有初始化數據)
Thread 2 :利用返回的instance做某些操做,失敗或者異常
Thread 1 :取得cpu初始化完成
過程中可能有多個線程取到了沒有完成的實例,並用這個實例作出某些操做。
-----------------------------------------
出現以上的問題是因爲
mem = allocate();             //分配內存
instance = mem;               //標記instance非空
                              //未執行構造函數,thread 2從這裏進入
ctorSingleton(instance);      //執行構造函數
                              //返回instance
------------------------------------------                             

五,證明上邊的假想是可能發生的,字節碼是用來分析問題的最好的工具,可以利用它來分析
下邊一段程序:(爲了分析方便,所以漸少了內容)
字節碼的使用方法見這裏,利用字節碼分析問題
class Singleton
{
  private static Singleton instance;
  private boolean inUse;
  private int val; 

  private Singleton()
  {
    inUse = true;
    val = 5;
  }
  public static Singleton getInstance()
  {
    if (instance == null)
      instance = new Singleton();
    return instance;
  }
}
得到的字節碼                           
;asm code generated for getInstance
054D20B0   mov         eax,[049388C8]      ;load instance ref
054D20B5   test        eax,eax             ;test for null
054D20B7   jne         054D20D7
054D20B9   mov         eax,14C0988h
054D20BE   call        503EF8F0            ;allocate memory
054D20C3   mov         [049388C8],eax      ;store pointer in
                                           ;instance ref. instance 
                                           ;non-null and ctor
                                           ;has not run
054D20C8   mov         ecx,dword ptr [eax]
054D20CA   mov         dword ptr [ecx],1   ;inline ctor - inUse=true;
054D20D0   mov         dword ptr [ecx+4],5 ;inline ctor - val=5;
054D20D7   mov         ebx,dword ptr ds:[49388C8h]
054D20DD   jmp         054D20B0

上邊的字節碼證明,猜想是有可能實現的

六:好了,上邊證明Double-checked locking可能出現取出錯誤數據的情況,那麼我們還是可以解決的
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3     ///搞不清楚爲什麼用兩次同步,同步加鎖應該是可以嵌套的。也就是說鎖不會在下個                                                     大括弧-aaaaa處,後面釋放調。如果鎖不可以嵌套,就是在下個大括弧-aaaaa處,釋                                                    放所有鎖的話,這時就和模式1一樣了,也會存在多個實例。
          inst = new Singleton();        //4
        }//aaaaa
        instance = inst;                 //5
      }
    }
  }
  return instance;
}
利用Double-checked locking 兩次同步,中間變量,解決上邊的問題。
(下邊這段話我只能簡單的理解,翻譯過來不好,所以保留原文,list 7是上邊的代碼,list 8是下邊的
The code in Listing 7 doesn't work because of the current definition of the memory model.
 The Java Language Specification (JLS) demands that code within a synchronized block
 not be moved out of a synchronized block. However, it does not say that
 code not in a synchronized block cannot be moved into a synchronized block.
A JIT compiler would see an optimization opportunity here.
This optimization would remove the code at
//4 and the code at //5, combine it and generate the code shown in Listing 8:)
-------------------------------------------------
list 8
public static Singleton getInstance()
{
  if (instance == null)
  {
    synchronized(Singleton.class) {      //1
      Singleton inst = instance;         //2
      if (inst == null)
      {
        synchronized(Singleton.class) {  //3
          //inst = new Singleton();      //4
          instance = new Singleton();              
        }
        //instance = inst;               //5
      }
    }
  }
  return instance;
}
If this optimization takes place, you have the same out-of-order write problem we discussed earlier.
如果這個優化發生,將再次發生上邊提到的問題,取得沒有實例化完成的數據。
-------------------------------------------------

以下部分爲了避免我翻譯錯誤誤導打家,保留原文

Another idea is to use the keyword volatile for the variables inst and instance.
According to the JLS (see Resources), variables declared volatile are supposed to
be sequentially consistent, and therefore, not reordered.
But two problems occur with trying to use volatile to fix the problem with
double-checked locking:

The problem here is not with sequential consistency.
Code is being moved, not reordered.

Many JVMs do not implement volatile correctly regarding sequential consistency anyway.
The second point is worth expanding upon. Consider the code in Listing 9:

Listing 9. Sequential consistency with volatile

class test
{
  private volatile boolean stop = false;
  private volatile int num = 0;

  public void foo()
  {
    num = 100;    //This can happen second
    stop = true;  //This can happen first
    //...
  }

  public void bar()
  {
    if (stop)
      num += num;  //num can == 0!
  }
  //...
}
 
According to the JLS, because stop and num are declared volatile,
they should be sequentially consistent. This means that if stop is ever true,
num must have been set to 100.
However, because many JVMs do not implement the sequential consistency feature of volatile,
you cannot count on this behavior.
Therefore, if thread 1 called foo and thread 2 called bar concurrently,
thread 1 might set stop to true before num is set to 100.
This could lead thread 2 to see stop as true, but num still set to 0.
There are additional problems with volatile and the atomicity of 64-bit variables,
but this is beyond the scope of this article.
See Resources for more information on this topic.

簡單的理解上邊這段話,使用volatile有可能能解決問題,volatile被定義用來保正一致性,但是很多虛擬機
並沒有很好的實現volatile,所以使用它也會存在問題。

最終的解決方案:
 (1),單態模式2,使用同步方法
 (2),放棄同步,使用一個靜態變量,如下
class Singleton
{
  private Vector v;
  private boolean inUse;
  private static Singleton instance = new Singleton();

  private Singleton()
  {
    v = new Vector();
    inUse = true;
    //...
  }

  public static Singleton getInstance()
  {
    return instance;
  }
}
但使用靜態變量也會存在問題,問題見 這篇文章

而且如在文章開頭提到的,使用EJB跨服務器,跨JVM的情況下,單態更是問題

好了是不是感覺單態模式根本沒法使用了,其實上邊都是特殊情況,這中特殊情況的出現是有條件的,只要
根據你的具體應用,迴避一些,就能解決問題,所以單態還是可以使用的。但是在使用前慎重,自己考慮好自己
的情況適合哪種情況。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章