2011年5月19日 星期四

Java - Singletone Pattern &Double-checked locking

今天看到一篇文章在比較ConcurrentHashMap and HashMap with synchronized 的效能, 另一篇文章又在說明ConcurrentHashMap 的內容實作,ConcurrentHashMap 裡面使用了一些技巧(JMM) , 看到JMM又跑去看了一下,後來又看到了Double-checked locking, 以往實作Singleton時不會考慮到的一些問題。

wiki裡面的例子, 基本的singleton 實作, 在單thread下
class Foo {
    private Helper helper = null;
    public synchronized Helper getHelper() {
        if (helper == null)
            helper = new Helper();
        return helper;
    }
 
    // other functions and members...
}

但是這個在multi-thread下是不行的, 所以會有一種解決方式:
// Correct multithreaded version
class Foo { 
  private Helper helper = null;
  public synchronized Helper getHelper() {
    if (helper == null) 
        helper = new Helper();
    return helper;
    }
  // other functions and members...
  }

但是這種寫法效率上又不是很好,所以會有另一種的改進方法
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other functions and members...
}

但是文章中又提到,上面這種看起來是work的,但是實際上是行不通的,因為new Helper()在很多平台和優化編譯器上是錯誤的。原因在於:helper = new Helper()這行代碼在不同編譯器上的行為是無法預知的。

看看另一個例子,
public  class  LazySingleton {  
    private  int  someField;  
      
    private  static  LazySingleton instance;  
      
    private  LazySingleton() {  
        this .someField = new  Random().nextInt( 200 )  1 ;      
    }  
      
    public  static  LazySingleton getInstance() {  
        if  (instance == null ) {                               
            synchronized (LazySingleton. class ) {           
                if  (instance == null ) {                    
                    instance = new  LazySingleton();        
                }  
            }  
        }  
        return  instance;                                  
    }  
      
    public  int  getSomeField() {  
        return  this .someField;                             
    }  
}  
這個例子下, 儘管得到了LazySingleton的正確的引用,但是卻有可能訪問到其成員變量的不正確值,具體來說LazySingleton.getInstance().getSomeField()有可能返回someField的默認值0。如果程序行為正確的話,這應當是不可能發生的事,因為在構造函數里設置的someField的值不可能為0。也說明這種情況理論上有可能發生。
DCL的分析也告訴我們一條經驗原則, 對引用(包括對象引用和數組引用)的非同步訪問,即使得到該引用的最新值,卻並不能保證也能得到其成員變量(對數組而言就是每個數組元素)的最新值。LazySingleton是一個不變class,它只有get方法而沒有set方法,即使對於不可變對象,它也必須被安全的發布,才能被安全地共享。 所謂“安全的共享”就是說不需要同步也不會遇到數據競爭的問題。 在Java5或以後,將someField聲明成final的,即使它不被安全的發布,也能被安全地共享,而在Java1.4或以前則必須被安全地發布。

這種設計上的問題要如何解決?
在JDK 1.5裡面,我們可以這樣做

// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
    private volatile Helper helper = null;
    public Helper getHelper() {
        Helper result = helper;
        if (result == null) {
            synchronized(this) {
                result = helper;
                if (result == null)
                    helper = result = new Helper();
            }
        }
        return result;
    }
 
    // other functions and members...
}
將object 設為volatile , 注意volatile只能保證多線程的內存可見性,不能保證多線程的執行有序性。
以下的a=a+count包含很多動作, 多個線程的執行是無序的,因為沒有任何機制來保證多個線程的執行有序性和原子性。 volatile存在的意義是,任何線程對a的修改,都會馬上被其他線程讀取到

public  class  VolatileTest{  
      public  volatile  int  a;  
      public  void  add( int  count){  
           a=a+count;  
           //a++;
      }  
    }  
在effective中提到的,簡單實現的方法
public  class  Singleton {  
      
      private  Singleton() {}  
      
      // Lazy initialization holder class idiom for static fields  
      private  static  class  InstanceHolder {  
       private  static  final  Singleton instance = new  Singleton();  
      }  
      
      public  static  Singleton getSingleton() {   
        return  InstanceHolder.instance;   
      }  
    }  

或著實作threadlocal
class Foo {
  /** If perThreadInstance.get() returns a non-null value, this thread
  has done synchronization needed to see initialization
  of helper */
         private final ThreadLocal perThreadInstance = new ThreadLocal();
         private Helper helper = null;
         public Helper getHelper() {
             if (perThreadInstance.get() == null) createHelper();
             return helper;
         }
         private final void createHelper() {
             synchronized(this) {
                 if (helper == null)
                     helper = new Helper();
             }
      // Any non-null value would do as the argument here
             perThreadInstance.set(perThreadInstance);
         }
 }

wiki 上的另一個例子,設成靜態的
// Correct lazy initialization in Java 
@ThreadSafe
class Foo {
    private static class HelperHolder {
       public static Helper helper = new Helper();
    }
    public static Helper getHelper() {
        return HelperHolder.helper;
    }
}

一樣是使用volatile 跟 final , (JDK 1.5 對volatile & final 有加強一些語意,針對JMM)
public class FinalWrapper<T>
{
    public final T value;
    public FinalWrapper(T value) { this.value = value; }
}
 
public class Foo
{
   private FinalWrapper<Helper> helperWrapper = null;
   public Helper getHelper()
   {
      FinalWrapper<Helper> wrapper = helperWrapper;
      if (wrapper == null)
      {
          synchronized(this)
          {
              if (helperWrapper ==null)
                  helperWrapper = new FinalWrapper<Helper>( new Helper() );
              wrapper = helperWrapper;
          }
      }
      return wrapper.value;
   }
}



More infomation :
Performance comparision between ConcurrentHashMap and synchronized HashMap in Terracotta

ConcurrentHashMap

Wikipedia : Double-checked locking

The "Double-Checked Locking is Broken" Declaration

Jeremy Manson Blog:Double Checked Locking

用happen-before規則重新審視DCL

沒有留言:

張貼留言