歡迎您光臨本站 註冊首頁

多線程編程您不知道的5件事

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0
多線程編程向來不容易,但很少有 Java? 開發人員能夠忽視多線程編程和支持它的 Java 平台庫我們臨時學習線程,在需要時向我們的工具箱添加新的技巧和技術.以這種方式構建和運行適當的應用程序是可行的,但是您可以做的不止這些.理解 Java 編譯器的線程處理特性和 JVM 將有助於您編寫更高效、性能更好的 Java 代碼.

在這篇文章中,我將通過同步方法、volatile 變數和原子類介紹多線程編程的一些更隱晦的方面.我的討論特別關注於這些構建如何與 JVM 和 Java 編譯器交互,以及不同的交互如何影響 Java 應用程序的性能.

1. 同步方法或同步代碼塊?

您可能偶爾會思考是否要同步化這個方法調用,還是只同步化該方法的線程安全子集.在這些情況下,知道 Java 編譯器何時將源代碼轉化為位元組代碼會很有用,它處理同步方法和同步代碼塊的方式完全不同.

當 JVM 執行一個同步方法時,執行中的線程識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設置,然後它自動獲取對象的鎖,調用方法,最后釋放鎖.如果有異常發生,線程自動釋放鎖.

另一方面,同步化一個方法塊會越過 JVM 對獲取對象鎖和異常處理的內置支持,要求以位元組代碼顯式寫入功能.如果您使用同步方法讀取一個方法的位元組代碼,就會看到有十幾個額外的操作用於管理這個功能.清單 1 展示用於生成同步方法和同步代碼塊的調用:

清單 1. 兩種同步化方法


synchronizedMethodGet() 方法生成以下位元組代碼:


這裡是來自 synchronizedBlockGet() 方法的位元組代碼:


創建同步代碼塊產生了 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 ;

上一條語句也可寫成:


換言之,如果一個易失性變數得到更新,這樣其值就會在底層被讀取、修改並分配一個新值,結果將是一個在兩個同步操作之間執行的非線程安全操作.然後您可以決定是使用同步化還是依賴於 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 類


Book 類僅是一個 POJO(Java 原生類對象),擁有一個單一欄位:name.

清單 3. MyObject 類


正如您所期望的,清單 3 中的 MyObject 類通過 get 和 set 方法公開其 whatAmIReading 屬性,但是 set 方法所做的有點不同.它不僅僅將其內部 Book 引用分配給指定的 Book(這將使用 清單 3 中註釋出的代碼來完成),而是使用一個 AtomicReferenceFieldUpdater.

AtomicReferenceFieldUpdater

AtomicReferenceFieldUpdater 的 Javadoc 將其定義為:

對指定類的指定易失性引用欄位啟用原子更新的一個基於映像的實用程序.該類旨在用於這樣的一個原子數據結構中:即同一節點的若干引用欄位獨立地得到原子更新.

在 清單 3 中,AtomicReferenceFieldUpdater 由一個對其靜態 newUpdater 方法的調用創建,該方法接受三個參數:

包含欄位的對象的類(在本例中為 MyObject)

將得到原子更新的對象的類(在本例中是 Book)

將經過原子更新的欄位的名稱

這裡真正的價值在於,getWhatImReading 方法未經任何形式的同步便被執行,而 setWhatImReading 是作為一個原子操作執行的.



清單 4 展示如何使用 setWhatImReading() 方法並斷定值的變動是正確的:

清單 4. 演習原子更新的測試用例


結束語

多線程編程永遠充滿了挑戰,但是隨著 Java 平台的演變,它獲得了簡化一些多線程編程任務的支持.在本文中,我討論了關於在 Java 平台上編寫多線程應用程序您可能不知道的 5 件事,包括同步化方法與同步化代碼塊之間的不同,為每個線程存儲運用 ThreadLocal 變數的價值,被廣泛誤解的 volatile 關鍵字(包括依賴於 volatile 滿足同步化需求的危險),以及對原子類的錯雜之處的一個簡要介紹.


[火星人 ] 多線程編程您不知道的5件事已經有558次圍觀

http://coctec.com/docs/java/show-post-59832.html