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 在通路易失性變量時确實隐式地提供同步,但是有一條重要提醒:讀取易失性變量是同步的,寫入易失性變量也是同步的,但非原子操作不同步。
這表示下面的代碼不是線程安全的:
|
上一條語句也可寫成:
|
換言之,如果一個易失性變量得到更新,這樣其值就會在底層被讀取、修改并配置設定一個新值,結果将是一個在兩個同步操作之間執行的非線程安全操作。然後您可以決定是使用同步化還是依賴于 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
滿足同步化需求的危險),以及對原子類的錯雜之處的一個簡要介紹。參見 參考資料 部分了解更多内容。