多線程編程向來不容易
但很少有 Java? 開發人員能夠忽視多線程編程和支持它的 Java 平台庫我們臨時學習線程
在需要時向我們的工具箱添加新的技巧和技術
以這種方式構建和運行適當的應用程序是可行的
但是您可以做的不止這些
理解 Java 編譯器的線程處理特性和 JVM 將有助於您編寫更高效
性能更好的 Java 代碼
在這篇文章中
我將通過同步方法
volatile 變量和原子類介紹多線程編程的一些更隱晦的方面
我的討論特別關注於這些構建如何與 JVM 和 Java 編譯器交互
以及不同的交互如何影響 Java 應用程序的性能
同步方法或同步代碼塊?
您可能偶爾會思考是否要同步化這個方法調用
還是只同步化該方法的線程安全子集
在這些情況下
知道 Java 編譯器何時將源代碼轉化為字節代碼會很有用
它處理同步方法和同步代碼塊的方式完全不同
當 JVM 執行一個同步方法時
執行中的線程識別該方法的 method_info 結構是否有 ACC_SYNCHRONIZED 標記設置
然後它自動獲取對象的鎖
調用方法
最後釋放鎖
如果有異常發生
線程自動釋放鎖
另一方面
同步化一個方法塊會越過 JVM 對獲取對象鎖和異常處理的內置支持
要求以字節代碼顯式寫入功能
如果您使用同步方法讀取一個方法的字節代碼
就會看到有十幾個額外的操作用於管理這個功能
清單
展示用於生成同步方法和同步代碼塊的調用
清單
兩種同步化方法
synchronizedMethodGet() 方法生成以下字節代碼
這裡是來自 synchronizedBlockGet() 方法的字節代碼
創建同步代碼塊產生了
行的字節碼
而創建同步方法僅產生了
行
ThreadLocal 變量
如果您想為一個類的所有實例維持一個變量的實例
將會用到靜態類成員變量
如果您想以線程為單位維持一個變量的實例
將會用到線程局部變量
ThreadLocal 變量與常規變量的不同之處在於
每個線程都有其各自初始化的變量實例
這通過 get() 或 set() 方法予以評估
比方說您在開發一個多線程代碼跟蹤器
其目標是通過您的代碼惟一標識每個線程的路徑
挑戰在於
您需要跨多個線程協調多個類中的多個方法
如果沒有 ThreadLocal
這會是一個復雜的問題
當一個線程開始執行時
它需要生成一個惟一的令牌來在跟蹤器中識別它
然後將這個惟一的令牌傳遞給跟蹤中的每個方法
使用 ThreadLocal
事情就變得簡單多了
線程在開始執行時初始化線程局部變量
然後通過每個類的每個方法訪問它
保證變量將僅為當前執行的線程托管跟蹤信息
在執行完成之後
線程可以將其特定的蹤跡傳遞給一個負責維護所有跟蹤的管理對象
當您需要以線程為單位存儲變量實例時
使用 ThreadLocal 很有意義
Volatile 變量
我估計
大約有一半的 Java 開發人員知道 Java 語言包含 volatile 關鍵字
當然
其中只有
% 知道它的確切含義
有更少的人知道如何有效使用它
簡言之
使用 volatile 關鍵字識別一個變量
意味著這個變量的值會被不同的線程修改
要完全理解 volatile 關鍵字的作用
首先應當理解線程如何處理非易失性變量
為了提高性能
Java 語言規范允許 JRE 在引用變量的每個線程中維護該變量的一個本地副本
您可以將變量的這些
線程局部
副本看作是與緩存類似
在每次線程需要訪問變量的值時幫助它避免檢查主存儲器
不過看看在下面場景中會發生什麼
兩個線程啟動
第一個線程將變量 A 讀取為
第二個線程將變量 A 讀取為
如果變量 A 從
變為
第一個線程將不會知道這個變化
因此會擁有錯誤的變量 A 的值
但是如果將變量 A 標記為 volatile
那麼不管線程何時讀取 A 的值
它都會回頭查閱 A 的原版拷貝並讀取當前值
如果應用程序中的變量將不發生變化
那麼一個線程局部緩存比較行得通
不然
知道 volatile 關鍵字能為您做什麼會很有幫助
易失性變量與同步化
如果一個變量被聲明為 volatile
這意味著它預計會由多個線程修改
當然
您會希望 JRE 會為易失性變量施加某種形式的同步
幸運的是
JRE 在訪問易失性變量時確實隱式地提供同步
但是有一條重要提醒
讀取易失性變量是同步的
寫入易失性變量也是同步的
但非原子操作不同步
這表示下面的代碼不是線程安全的
myVolatileVar++;
上一條語句也可寫成
換言之
如果一個易失性變量得到更新
這樣其值就會在底層被讀取
修改並分配一個新值
結果將是一個在兩個同步操作之間執行的非線程安全操作
然後您可以決定是使用同步化還是依賴於 JRE 的支持來自動同步易失性變量
更好的方法取決於您的用例
如果分配給易失性變量的值取決於當前值(比如在一個遞增操作期間)
要想該操作是線程安全的
那麼您必須使用同步化
原子字段更新程序
在一個多線程環境中遞增或遞減一個原語類型時
使用在 ncurrent
atomic 包中找到的其中一個新原子類比編寫自己的同步代碼塊要好得多
原子類確保某些操作以線程安全方式被執行
比如遞增和遞減一個值
更新一個值
添加一個值
原子類列表包括 AtomicInteger
AtomicBoolean
AtomicLong
AtomicIntegerArray 等等
使用原子類的難題在於
所有類操作
包括 get
set 和一系列 get
set 操作是以原子態呈現的
這表示
不修改原子變量值的 read 和 write 操作是同步的
不僅僅是重要的 read
update
write 操作
如果您希望對同步代碼的部署進行更多細粒度控制
那麼解決方案就是使用一個原子字段更新程序
使用原子更新
像 AtomicIntegerFieldUpdater
AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 之類的原子字段更新程序基本上是應用於易失性字段的封裝器
Java 類庫在內部使用它們
雖然它們沒有在應用程序代碼中得到廣泛使用
但是也沒有不能使用它們的理由
清單
展示一個有關類的示例
該類使用原子更新來更改某人正在讀取的書目
清單
Book 類
Book 類僅是一個 POJO(Java 原生類對象)
擁有一個單一字段
name
清單
MyObject 類
正如您所期望的
清單
中的 MyObject 類通過 get 和 set 方法公開其 whatAmIReading 屬性
但是 set 方法所做的有點不同
它不僅僅將其內部 Book 引用分配給指定的 Book(這將使用 清單
中注釋出的代碼來完成)
而是使用一個 AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater
AtomicReferenceFieldUpdater 的 Javadoc 將其定義為
對指定類的指定易失性引用字段啟用原子更新的一個基於映像的實用程序
該類旨在用於這樣的一個原子數據結構中
即同一節點的若干引用字段獨立地得到原子更新
在 清單
中
AtomicReferenceFieldUpdater 由一個對其靜態 newUpdater 方法的調用創建
該方法接受三個參數
包含字段的對象的類(在本例中為 MyObject)
將得到原子更新的對象的類(在本例中是 Book)
將經過原子更新的字段的名稱
這裡真正的價值在於
getWhatImReading 方法未經任何形式的同步便被執行
而 setWhatImReading 是作為一個原子操作執行的
清單
展示如何使用 setWhatImReading() 方法並斷定值的變動是正確的
清單
演習原子更新的測試用例
結束語
多線程編程永遠充滿了挑戰
但是隨著 Java 平台的演變
它獲得了簡化一些多線程編程任務的支持
在本文中
我討論了關於在 Java 平台上編寫多線程應用程序您可能不知道的
件事
包括同步化方法與同步化代碼塊之間的不同
為每個線程存儲運用 ThreadLocal 變量的價值
被廣泛誤解的 volatile 關鍵字(包括依賴於 volatile 滿足同步化需求的危險)
以及對原子類的錯雜之處的一個簡要介紹
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27308.html