为什么我们需要volatile关键字?
volatile字段以確保多個線程始終看到最新值,即使緩存系統或編譯器優化正在起作用。從volatile變量讀取始終返回此變量的最新寫入值。java.uti.concurrent包中的大多數類的方法也具有此屬性。通常在內部使用volatile字段。
關于volatile關鍵字讓我著迷的是它是必須的,因為我的軟件仍然在硅芯片上運行。即使我的應用程序在Java虛擬機中的虛擬機上運行在云中。但是,盡管所有這些軟件層都抽象掉底層硬件,但由于我的軟件運行的處理器緩存,仍然需要volatile關鍵字。
處理器會在每個內核緩存中緩存主內存值,這樣提高內存訪問性能。雖然從CPU寄存器讀取大約300皮秒,但從主存儲器讀取需要50-100納秒。通過使用高速緩存,可以減少到大約1納秒。
現在問題是核心應該何時檢查緩存的值是否在另一個核的緩存中被修改了,這是由volatile字段注釋完成的。通過將字段聲明為volatile,我們告訴JVM,當線程讀取volatile字段時,我們希望看到最新的寫入值。JVM使用特殊指令告訴CPU它應該同步其緩存。對于x86處理器系列,這些指令稱為內存屏障,如此處所述。
處理器不僅可以同步volatile字段的值,還可以同步整個緩存。因此,如果我們從volatile字段讀取,我們會看到其他內核上的所有寫入此變量以及寫入volatile變量之前寫入這些內核的值。
測試
現在讓我們看看它在實踐中是如何運作的。讓我們看看當我們使用沒有volatile注釋的字段時我們是否讀取過時的值:
public?class?Termination?{private?int?v;public?void?runTest()?throws?InterruptedException?{Thread workerThread =?new?Thread( () -> {while(v ==?0) {// spin}});workerThread.start();v =?1;workerThread.join();?// test might hang up here}public?static?void?main(String[] args)??throws?InterruptedException?{for(int?i =?0?; i <?1000?; i++) {new?Termination().runTest();}} }當在一個核中寫入線程實現更新字段v,同時讀取線程在另一個線程中讀取字段v時,測試時應該掛起并永遠運行。但至少當我在我的機器上運行測試時,測試永遠不會掛起。原因是測試需要很少的CPU周期,兩個線程通常在同一個內核上運行。當兩個線程在同一個內核上運行時,它們會讀取并寫入同一個緩存。
幸運的是,OpenJDK提供了一個工具jcstress,它可以幫助進行這類測試。jcstress使用多個技巧,測試的線程在不同的核心上運行。這里上面的例子被重寫為jcstress測試:
@JCStressTest(Mode.Termination) @Outcome(id =?"TERMINATED", expect = Expect.ACCEPTABLE, desc =?"Gracefully finished.") @Outcome(id =?"STALE", expect = Expect.ACCEPTABLE_INTERESTING, desc =?"Test hung up.") @State public?class?APISample_03_Termination?{int?v;@Actorpublic?void?actor1()?{while?(v ==?0) {// spin}}@Signalpublic?void?signal()?{v =?1;} }此測試來自jcstress示例。通過使用注釋@JCStressTest注釋類,我們告訴jcstress這個類是一個jcstress測試。jcstress在一個單獨的線程中運行用@Actor和@Signal注釋的方法。jcstress首先啟動actor線程,然后運行信號線程。如果測試在合理的時間內退出,jcstress會記錄“TERMINATED”結果,否則結果為“STALE”。
我已經在我的開發機器上運行了這個測試,一次使用普通測試,一次使用volatile字段v。對于volatile字段的測試看起來像這樣:
public?class?APISample_03_Termination?{volatile?int?v;// methods omitted }jcstress使用不同的JVM參數多次運行測試用例。
測試結果表明:使用沒有volatile注釋的字段確實會掛起線程。掛起線程的百分比取決于JVM標志和環境,JDK版本等。
何時使用volatile字段
volatile字段的另一種用法是使用volatile字段進行讀取和鎖定以進行寫入。或者您可以將它們與JDK 9 VarHandle一起使用以實現原子操作。這里描述了如何實現這些技術。
與happens-before相關
一般情況下人們不直接使用volatile字段。我寧愿使用java.util.concurrent包中的數據結構進行并發編程。其中內部使用volatile字段。
在這些類的文檔中,我們經常閱讀關于內存一致性效果的事情,與happens-before有關:
內存一致性效果:happen-before異步計算所采取的操作發生在另一個線程中相應的Future.get()之后。
現在,憑借我們對volatile字段的了解,我們可以解碼此文檔。如果我們從volatile字段讀取,我們會看到其他內核上的所有寫入此變量。用java.util.concurrent文檔的話來說,我們會說對volatile變量的讀取會創建happen-before關系到此變量的寫入。
所以上面的語句意味著調用Future.get()的線程總是會讀取在另外一個線程調用Future接口方法的寫入的最新寫入值。
我們使用FutureTask類在兩個線程之間傳輸數據作為示例。FutureTask實現接口Future,因此調用方法FutureTask.get()總是能看到通過另一個方法(例如FutureTask.set())寫入的最新值。
用于檢測缺少的volatile注釋的工具
如果您忘記將字段聲明為volatile,則線程可能會讀取過時的值。但是在測試期間看到這個的機會相當低。由于讀取和寫入必須幾乎在同一時間并且在不同的核心上發生以讀取過時值,因此這僅在重負載和長時間運行之后發生,例如在生產中。
因此,在測試運行中存在檢測此類問題的工具并不奇怪:
ThreadSanitizer可以檢測C ++程序中缺少的volatile注釋。有一個Java增強提議草案,JEP草案:Java Thread Sanitizer將ThreadSanitizer包含在OpenJDK JVM中。這將允許我們在JVM中以及在JVM執行的Java應用程序中找到缺少的volatile注釋。
vmlens是我編寫的用于測試并發java的工具,它可以檢測Java測試運行中缺少的volatile注釋。
總結
以上是生活随笔為你收集整理的为什么我们需要volatile关键字?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分享一个牛逼的阿里天猫面经,已经拿到 O
- 下一篇: 这款多线程中间件,吊打 Redis!