2011-01-06 88 views
4

我跑進我的應用程序的一些鎖定問題,其中包括像下面幾類:靜態初始化和靜態同步方法鎖定問題

public interface AppClient { 
    void hello(); 
} 

public class Client implements AppClient { 
    public synchronized static AppClient getInstance() { 
     return instance; 
    } 

    public void hello() { 
     System.out.println("Hello Client"); 
    } 

    private final static class InnerClient implements AppClient { 
     public void hello() { 
      System.out.println("Hello InnerClient"); 
     } 
    } 
    private static AppClient instance; 

    static { 
     instance = new InnerClient(); 
     doSomethingThatWillCallClientGetInstanceSeveralTimes(); 
    } 
} 

public class Application { 
    new Thread() { 
     AppClient c = Client.getInstance(); 
     c.hello(); 
    }.start(); 
    new Thread() { 
     AppClient c = Client.getInstance(); 
     c.hello(); 
    }.start(); 
    // ... 
    new Thread() { 
     AppClient c = Client.getInstance(); 
     c.hello(); 
    }.start(); 
} 

在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法,它會做相當多的初始化涉及很多類的工作,並且在初始化期間多次循環調用Client.getInstance靜態方法(我理解這並不好,但是,這是一個持續20多年的遺留代碼庫)。

這裏是我的問題:

1)我認爲類客戶端初始化完成之前,只觸發類初始化客戶端可以訪問Client.getInstance方法,因爲JVM將Client.class對象上進行同步的第一個線程在類初始化完成之前。我閱讀了關於相關主題的JLS並得出了這個結論(第12.4.2節,詳細初始化過程,http://java.sun.com/docs/books/jls/third_edition/html/execution.html)。

2)但是,這不是我在真實環境中看到的行爲。例如,有三個線程調用Client.getInstance(),線程1觸發Client.class初始化,並在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法中多次調用Client.getInstance()。並且在完成doSomethingThatWillCallClientGetInstanceSeveralTimes()方法之前,線程2獲取Client.class對象的鎖定(這怎麼可能?但它確實發生了),並且進入Client.getInstance方法(因爲此方法是靜態同步方法) 。出於某種原因,線程2不能返回「實例」(我猜它正在等待Client.class完成其初始化)。同時,thread-1無法繼續,因爲它仍然需要在doSomethingThatWillCallClientGetInstanceSeveralTimes()中調用Client.getInstance,並且由於它由線程2擁有,所以無法獲取該鎖。線程轉儲告訴我,線程2處於RUNNABLE狀態,線程1處於BLOCKED狀態,等待線程2擁有的鎖。

我只能在Windows中的64位Java 6u23 JVM中重現此行爲,無法將其重現爲32位Java 6 JVM + Windows環境。有人能告訴我我在這裏錯過了什麼嗎?這樣的代碼註定會引起這種鎖定,如果是的話,怎麼會來?我對這部分的JLS理解是不正確的?或者這是一個JVM問題?任何幫助表示讚賞。謝謝。

+0

你的代碼有點令人困惑,特別是當Client和InnerClient實現AppClient時 - 我看不出客戶需要什麼。如果你可以創建一個簡短但完整的程序來演示這個問題,那將會有很大的幫助。 – 2011-01-06 07:01:56

+0

@Jon我無法創建一個簡單的程序來重現此問題。上面的程序是我從一個巨大的庫中提取的骨架代碼,我認爲問題在於此。這是因爲向後可比較性,Client和InnerClient實現AppClient,並且我不想錯過任何重要信息成爲這個問題的原因,所以我列在那裏。也許我需要更準確的時機和更多的運氣來用簡單的程序來演示這個問題,無論如何,我只會發現我列出的當前程序的行爲將像上面提到的那樣。 – nybon 2011-01-06 07:20:44

+0

那麼我會專注於以受控的方式重現它 - 因爲如果沒有這個,我們真的只是在猜測。正如你所說,它聽起來像*應該*工作... – 2011-01-06 07:22:53

回答

1

的JLS 12.4 .2在(6)中明確指出,,而初始化器執行,loc ks被釋放。所以我認爲你會看到一個有效的執行路徑。您可能是

  • 更好的查殺靜態初始化,並在訪問做同步懶初始化
  • 手動同步靜態初始化代碼
 
    public synchronized static AppClient getInstance() { 
     synchronized(Client.class) { 
      if (instance == null) { 
      instance = new InnerClient(); 
      doSomethingThatWillCallClientGetInstanceSeveralTimes(); 
      } 
      return instance; 
     } 
    } 

編輯

更妙的是 - 以後在spec中重新閱讀這一段,甚至有可能完全去除的同步在您的原始示例中 - 虛擬機將處理它。

EDIT

對不起誤導 - 更詳細的讀數應該顯示,在(2)中,第二線程不能獲得鎖(檢測正在進行初始化,則「等待」之後)。

3

對我來說,看起來像一個錯誤。當一個線程正在調用靜態塊時,其他線程無法訪問它。這個錯誤可能是另一個線程可以在初始化完成之前獲得該類的鎖。 :(

我建議你構造你的代碼,這樣你就不需要在啓動時加鎖了,它聽起來相當複雜,例如在你的例子中客戶端不需要擴展客戶端,並且實例可以初始化行其宣佈,我會考慮以下結構。

enum Client implements AppClient { 
    INSTANCE; 

    public void hello() { 
     System.out.println("Hello Client"); 
    } 
} 

你可以讓客戶端可變或使用委派所以它不公開它可以改變狀態(或執行)的事實