关于多线程编程您不知道的 5 件事 有关高性能线程处理的微妙之处
雖然很少有 Java? 開發人員能夠忽視多線程編程和支持它的 Java 平臺庫,更少有人有時間深入研究線程。相反地,我們臨時學習線程,在需要時向我們的工具箱添加新的技巧和技術。以這種方式構建和運行適當的應用程序是可行的,但是您可以做的不止這些。理解 Java 編譯器的線程處理特性和 JVM 將有助于您編寫更高效、性能更好的 Java 代碼。
在這期的?5 件事?系列?中,我將通過同步方法、volatile 變量和原子類介紹多線程編程的一些更隱晦的方面。我的討論特別關注于這些構建如何與 JVM 和 Java 編譯器交互,以及不同的交互如何影響 Java 應用程序的性能。
1. 同步方法或同步代碼塊?
您可能偶爾會思考是否要同步化這個方法調用,還是只同步化該方法的線程安全子集。在這些情況下,知道 Java 編譯器何時將源代碼轉化為字節代碼會很有用,它處理同步方法和同步代碼塊的方式完全不同。
當 JVM 執行一個同步方法時,執行中的線程識別該方法的?method_info?結構是否有?ACC_SYNCHRONIZED?標記設置,然后它自動獲取對象的鎖,調用方法,最后釋放鎖。如果有異常發生,線程自動釋放鎖。
另一方面,同步化一個方法塊會越過 JVM 對獲取對象鎖和異常處理的內置支持,要求以字節代碼顯式寫入功能。如果您使用同步方法讀取一個方法的字節代碼,就會看到有十幾個額外的操作用于管理這個功能。清單 1 展示用于生成同步方法和同步代碼塊的調用:
清單 1. 兩種同步化方法
| package com.geekcap;public class SynchronizationExample {private int i;public synchronized int synchronizedMethodGet() {return i;}public int synchronizedBlockGet() {synchronized( this ) {return i;}} } |
synchronizedMethodGet()?方法生成以下字節代碼:
| 0: aload_01: getfield2: nop3: iconst_m14: ireturn |
這里是來自?synchronizedBlockGet()?方法的字節代碼:
| 0: aload_01: dup2: astore_13: monitorenter4: aload_05: getfield6: nop7: iconst_m18: aload_19: monitorexit10: ireturn11: astore_212: aload_113: monitorexit14: aload_215: athrow |
創建同步代碼塊產生了 16 行的字節碼,而創建同步方法僅產生了 5 行。
回頁首
2. ThreadLocal 變量
如果您想為一個類的所有實例維持一個變量的實例,將會用到靜態類成員變量。如果您想以線程為單位維持一個變量的實例,將會用到線程局部變量。ThreadLocal?變量與常規變量的不同之處在于,每個線程都有其各自初始化的變量實例,這通過?get()?或?set()?方法予以評估。
比方說您在開發一個多線程代碼跟蹤器,其目標是通過您的代碼惟一標識每個線程的路徑。挑戰在于,您需要跨多個線程協調多個類中的多個方法。如果沒有?ThreadLocal,這會是一個復雜的問題。當一個線程開始執行時,它需要生成一個惟一的令牌來在跟蹤器中識別它,然后將這個惟一的令牌傳遞給跟蹤中的每個方法。
使用?ThreadLocal,事情就變得簡單多了。線程在開始執行時初始化線程局部變量,然后通過每個類的每個方法訪問它,保證變量將僅為當前執行的線程托管跟蹤信息。在執行完成之后,線程可以將其特定的蹤跡傳遞給一個負責維護所有跟蹤的管理對象。
當您需要以線程為單位存儲變量實例時,使用?ThreadLocal?很有意義。
回頁首
3. Volatile 變量
我估計,大約有一半的 Java 開發人員知道 Java 語言包含?volatile?關鍵字。當然,其中只有 10% 知道它的確切含義,有更少的人知道如何有效使用它。簡言之,使用?volatile?關鍵字識別一個變量,意味著這個變量的值會被不同的線程修改。要完全理解?volatile關鍵字的作用,首先應當理解線程如何處理非易失性變量。
為了提高性能,Java 語言規范允許 JRE 在引用變量的每個線程中維護該變量的一個本地副本。您可以將變量的這些 “線程局部” 副本看作是與緩存類似,在每次線程需要訪問變量的值時幫助它避免檢查主存儲器。
不過看看在下面場景中會發生什么:兩個線程啟動,第一個線程將變量 A 讀取為 5,第二個線程將變量 A 讀取為 10。如果變量 A 從 5 變為 10,第一個線程將不會知道這個變化,因此會擁有錯誤的變量 A 的值。但是如果將變量 A 標記為?volatile,那么不管線程何時讀取 A 的值,它都會回頭查閱 A 的原版拷貝并讀取當前值。
如果應用程序中的變量將不發生變化,那么一個線程局部緩存比較行得通。不然,知道?volatile?關鍵字能為您做什么會很有幫助。
回頁首
4. 易失性變量與同步化
如果一個變量被聲明為?volatile,這意味著它預計會由多個線程修改。當然,您會希望 JRE 會為易失性變量施加某種形式的同步。幸運的是,JRE 在訪問易失性變量時確實隱式地提供同步,但是有一條重要提醒:讀取易失性變量是同步的,寫入易失性變量也是同步的,但非原子操作不同步。
這表示下面的代碼不是線程安全的:
| myVolatileVar++; |
上一條語句也可寫成:
| int temp = 0; synchronize( myVolatileVar ) {temp = myVolatileVar; }temp++;synchronize( myVolatileVar ) {myVolatileVar = temp; } |
換言之,如果一個易失性變量得到更新,這樣其值就會在底層被讀取、修改并分配一個新值,結果將是一個在兩個同步操作之間執行的非線程安全操作。然后您可以決定是使用同步化還是依賴于 JRE 的支持來自動同步易失性變量。更好的方法取決于您的用例:如果分配給易失性變量的值取決于當前值(比如在一個遞增操作期間),要想該操作是線程安全的,那么您必須使用同步化。
回頁首
5. 原子字段更新程序
在一個多線程環境中遞增或遞減一個原語類型時,使用在?java.util.concurrent.atomic?包中找到的其中一個新原子類比編寫自己的同步代碼塊要好得多。原子類確保某些操作以線程安全方式被執行,比如遞增和遞減一個值,更新一個值,添加一個值。原子類列表包括?AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray?等等。
使用原子類的難題在于,所有類操作,包括?get、set?和一系列?get-set?操作是以原子態呈現的。這表示,不修改原子變量值的?read和?write?操作是同步的,不僅僅是重要的?read-update-write?操作。如果您希望對同步代碼的部署進行更多細粒度控制,那么解決方案就是使用一個原子字段更新程序。
使用原子更新
像?AtomicIntegerFieldUpdater、AtomicLongFieldUpdater?和?AtomicReferenceFieldUpdater?之類的原子字段更新程序基本上是應用于易失性字段的封裝器。Java 類庫在內部使用它們。雖然它們沒有在應用程序代碼中得到廣泛使用,但是也沒有不能使用它們的理由。
清單 2 展示一個有關類的示例,該類使用原子更新來更改某人正在讀取的書目:
清單 2. Book 類
| package com.geeckap.atomicexample;public class Book {private String name;public Book(){}public Book( String name ){this.name = name;}public String getName(){return name;}public void setName( String name ){this.name = name;} } |
Book?類僅是一個 POJO(Java 原生類對象),擁有一個單一字段:name。
清單 3. MyObject 類
| package com.geeckap.atomicexample;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/**** @author shaines*/ public class MyObject {private volatile Book whatImReading;private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =AtomicReferenceFieldUpdater.newUpdater( MyObject.class, Book.class, "whatImReading" );public Book getWhatImReading(){return whatImReading;}public void setWhatImReading( Book whatImReading ){//this.whatImReading = whatImReading;updater.compareAndSet( this, this.whatImReading, whatImReading );} } |
正如您所期望的,清單 3?中的?MyObject?類通過?get?和?set?方法公開其?whatAmIReading?屬性,但是?set?方法所做的有點不同。它不僅僅將其內部?Book?引用分配給指定的?Book(這將使用?清單 3?中注釋出的代碼來完成),而是使用一個AtomicReferenceFieldUpdater。
AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater?的 Javadoc 將其定義為:
對指定類的指定易失性引用字段啟用原子更新的一個基于映像的實用程序。該類旨在用于這樣的一個原子數據結構中:即同一節點的若干引用字段獨立地得到原子更新。在?清單 3?中,AtomicReferenceFieldUpdater?由一個對其靜態?newUpdater?方法的調用創建,該方法接受三個參數:
- 包含字段的對象的類(在本例中為?MyObject)
- 將得到原子更新的對象的類(在本例中是?Book)
- 將經過原子更新的字段的名稱
這里真正的價值在于,getWhatImReading?方法未經任何形式的同步便被執行,而?setWhatImReading?是作為一個原子操作執行的。
清單 4 展示如何使用?setWhatImReading()?方法并斷定值的變動是正確的:
清單 4. 演習原子更新的測試用例
| package com.geeckap.atomicexample;import org.junit.Assert; import org.junit.Before; import org.junit.Test;public class AtomicExampleTest {private MyObject obj;@Beforepublic void setUp(){obj = new MyObject();obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );}@Testpublic void testUpdate(){obj.setWhatImReading( new Book( "Pro Java EE 5 Performance Management and Optimization" ) );Assert.assertEquals( "Incorrect book name", "Pro Java EE 5 Performance Management and Optimization", obj.getWhatImReading().getName() );}} |
參閱?參考資料?了解有關原子類的更多信息。
回頁首
結束語
多線程編程永遠充滿了挑戰,但是隨著 Java 平臺的演變,它獲得了簡化一些多線程編程任務的支持。在本文中,我討論了關于在 Java 平臺上編寫多線程應用程序您可能不知道的 5 件事,包括同步化方法與同步化代碼塊之間的不同,為每個線程存儲運用ThreadLocal?變量的價值,被廣泛誤解的?volatile?關鍵字(包括依賴于?volatile?滿足同步化需求的危險),以及對原子類的錯雜之處的一個簡要介紹。參見?參考資料?部分了解更多內容。
參考資料
學習
- 您不知道的 5 件事?...?:在本系列中發現關于 Java 平臺您不知道的事情,本系列致力于將 Java 技術瑣事變成有用的編程技巧。
- “Code Tracing”(Steven Haines,InformIT,2010 年 8 月):了解使用?ThreadLocal?變量進行代碼跟蹤的更多內容。
- “Java 字節碼:了解字節碼使你成為一個更好的程序員”(Peter Haggar,developerWorks,2001 年 7 月):一本介紹字節碼次要領域的教程,包含展示同步方法和同步代碼塊之間區別的一個較早的例子。
- “Java 理論與實踐:流行的原子”(Brian Goetz,developerWorks,2004 年 11 月):解釋原子類如何支持用 Java 語言開發高度可伸縮的非阻塞算法。
- “Java 理論與實踐: 并發在一定程度上使一切變得簡單”(Brian Goetz,developerWorks,2002 年 11 月): 通過java.util.concurrent?包來為您提供指導。
- “您不知道的 5 件事?... java.util.concurrent,第 1 部分”(Ted Neward,developerWorks,2010 年 5 月):了解 5 個并發集合類,為您的并發編程需求改進標準集合類。
- developerWorks Java technology 專區:這里有數百篇關于 Java 編程各個方面的文章。
討論
- 加入?developerWorks 中文社區。
關于作者
Steven Haines 是 ioko 的一名技術架構師,也是 GeekCap Inc 的創始人。在 Java 編程和性能分析方面,他寫過 3 本書,以及上百篇文章和十幾個白皮書。Steven 還在行業會議上發表演講,比如 JBoss World 和 STPCon,而且他曾在加里佛尼亞大學歐文分校和 Learning Tree 大學教過 Java 編程,他居住在佛羅里達州奧蘭多市。
建議
總結
以上是生活随笔為你收集整理的关于多线程编程您不知道的 5 件事 有关高性能线程处理的微妙之处的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vc++操作mysql数据库的技巧
- 下一篇: 序列化包含多种不明类型的集合