2010-06-10 75 views
1

我覺得我有在Java中volatile關鍵字一個不錯的主意,但我想重新分解了一些代碼,我認爲這將是使用它是一個好主意。正確使用volatile關鍵字

我有一個類,基本上工作作爲一個數據庫緩存。它擁有一堆的對象,它已經從數據庫中讀取,用於對那些對象的請求,然後偶爾刷新數據庫(基於超時)。繼承人的骨架

public class Cache 
{ 
    private HashMap mappings =....; 
    private long last_update_time; 
    private void loadMappingsFromDB() 
    { 
     //.... 
    } 
    private void checkLoad() 
    { 
     if(System.currentTimeMillis() - last_update_time > TIMEOUT) 
      loadMappingsFromDB(); 
    } 
    public Data get(ID id) 
    { 
     checkLoad(); 
     //.. look it up 
    } 
} 

所以值得關注的是loadMappingsFromDB可能是一個高延遲操作和多數民衆無法接受的,所以最初我以爲我可以旋轉了一個線程緩存啓動,然後只是有它睡覺,然後更新在後臺緩存。但後來我需要同步我的課程(或地圖)。然後我只是偶爾進行一次大的停頓,讓每個緩存訪問速度變慢。

當時我就想,爲什麼不使用volatile

我可以定義繪圖參照揮發性

private volatile HashMap mappings =....; 

,然後在get(或其他地方使用該映射變量)我只想讓本地參考副本:

public Data get(ID id) 
{ 
    HashMap local = mappings; 
    //.. look it up using local 
} 

然後後臺線程會加載到臨時表中,然後交換r類別中的推理

HashMap tmp; 
//load tmp from DB 
mappings = tmp;//swap variables forcing write barrier 

這種方法是否有意義?它實際上是線程安全的嗎?

回答

2

有在現有的回答這個問題的一些誤傳。使用volatile實際上是確保線程安全性的一個很好的步驟。參見IBM的Peter Haggar的item 3 in Dispelling Java programming language myths。 Haggar給出了一些背景和示例,但是堅果是這樣的:

那麼,原子操作怎麼能不是線程安全的呢?主要的一點是它們可能確實是線程安全的,但不能保證它們是。允許Java線程將變量的私有副本與主內存分開。

通過使用volatile,您將保證線程指向主內存,而不是使用您不知道或期望的變量的私有副本。

要回答你的問題,那麼:是的,你的策略是安全的。

編輯:
爲了應對另一篇文章,這裏是the JLS section about volatile fields

+0

多數民衆贊成在一篇不錯的文章,謝謝 – luke 2010-06-10 17:11:18

0

我認爲這種一般的方法是有效的(重新加載緩存在後臺線程,而不是阻止訪問緩存,而負載是通過將數據裝入進步分離實例),但我不知道什麼聲明參考爲volatile真的給你。

您可以輕鬆地使用get(id)方法和loadMappingsFromDB()中覆蓋引用(而不是來自DB的全部加載,但只是重新分配mappings)在同一個鎖上同步的部分。

老實說,雖然我會考慮重新使用已建立的緩存庫,例如EhCache,它具有後臺加載或啓動時加載功能,因爲很久以前,這些庫可能已經解決了所有的同步問題。可以回去擔心你的應用程序的邏輯,而不是自釀緩存的低級別的安全性。

+0

我不想使用同步塊,因爲它們會導致性能開銷。 – luke 2010-06-10 16:47:41

+0

塊的開銷像'synchronized(lockObject){mappings = temp; }「遠遠低於你的想象。並且使用volatile還具有開銷。 – 2010-06-10 17:31:54

+0

易失性僅在寫入後立即產生開銷,因爲緩存已失效(這實際上取決於底層架構,但這通常是屏障的工作原理)。雖然同步,施加相同的懲罰加上一些方法調用和其他cheks以及潛在的阻塞,而揮發性總是會立即讀取值。 – luke 2010-06-10 19:39:20

-1

實際上,你提出的做法是有道理的,即使沒有使用「揮發性」關鍵字。由於參考分配(mappings = tmp;)是一個原子操作(轉換爲一個機器指令)沒有記憶不一致的機會:
http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html
讀取和寫入原子參考變量和最原始的變量(所有類型除了長和雙)。

'volatile'關鍵字後面的想法是,如果你在時間T改變這個變量,任何其他在時間T + x(x> 0)訪問它的線程都會看到新的值。否則,即使在更改值後,仍有一段時間,其他線程可能會看到舊值。但是這對你來說似乎不成問題。
編輯
相同的想法在上面的鏈接中描述。

+0

如果映射不是「易失性」,它將不會是「安全發佈」。引用不會立即更新,並且值可能會過時。 – gustafc 2010-06-10 16:57:07

+0

原子操作不能保證是線程安全的:http://www.ibm.com/developerworks/java/library/it/it-1001art24/index。html#3 – Pops 2010-06-10 16:58:30

+0

@gustafc如果我正確理解海報,幾秒鐘的延遲(這不太可能)不會成爲一個問題。這並不像價值會在一年內「過時」。但我同意在許多情況下這是不可取的。我剛纔描述了這個行爲,這是作者決定是否使用它。 – 2010-06-10 17:00:02

1

這種方法是否有意義?它實際上是線程安全的嗎?

它確實有意義,它是線程安全的。無論如何,在某種程度上。有些事情需要考慮:

  • 更新時,讓應用程序讀取過時的舊值。這是你的意圖嗎?對於某些應用程序很好,在其他情況下,您可能希望阻塞,直到更新緩存(FutureTask使此行爲變得相當容易)。
  • loadMappingsFromDB()開始時,最初調用get(ID)的線程將會阻塞,直到更新完成。
  • 有幾個線程可能會同時調用checkLoad(),這意味着如果重新加載速度很慢,並且您有多個線程調用get(ID),則最終可能會產生大量併發更新。雖然結果是一樣的,但這會浪費系統資源。一個簡單的方法來解決它在當前的代碼將有一個AtomicBoolean您在更新前檢查:

    private final AtomicBoolean isUpdating = new AtomicBoolean(false); 
    private void checkLoad() 
    { 
        if (System.currentTimeMillis() - last_update_time <= TIMEOUT) return; 
        if (!isUpdating.compareAndSet(false, true)) return; // already updating 
        try { 
         loadMappingsFromDB(); 
        } finally { 
         isUpdating.set(false); 
        } 
    } 
    
+0

@gustfc這些都是一般的優點,但它們對我的情況並不重要。只有一個線程會調用get,因此只有該線程纔會一次調用checkUpdate,問題更多的是修改此共享變量的安全性。此外,它也可以,如果有'陳舊'的讀取,因爲我們只是在輪詢更改,所以數據可能已經過時了一段時間,然後我們嘗試了更新(對於我的用例來說,這沒問題)。但感謝代碼片段,我沒有使用AtomicBoolean之前,所以+1 :) – luke 2010-06-10 19:36:35