儘管很少有Java開發人員能夠忽略多線程編程,且Java平台類庫支援它,甚至於更少的開發人員能有時間去深入學習線程。相反,我們只是泛泛地學習線程,如果需要的話,會向我們的工具箱中添加新的技巧和技術。通過這種方法你可能會構建且運行好的應用程式,但你還能做得更好。理解Java編譯器和JVM的線程特性,可以協助你編寫更高效,效能更佳的Java代碼。
在5
things系列
的本期文章中,我會介紹一些使用同步方法,volatile變數和原子類等多線程編程的細節方面。我的討論特別關注在這些程式結構是如何與JVM和Java編譯器進行互動的,以及不同的互動是如何影響Java應用程式效能的。
1. 同步方法與同步塊
你偶爾會衡量是否同步整個方法調用,或者只是同步方法中線程完全的子塊。在這種情況下,知道Java編譯器在何時將原始碼轉化為位元組碼是有協助的,它在處理同步方法和同步塊時是完全不同的。
當JVM在執行同步方法時,執行線程標識方法的method_info結構設有ACC_SYNCHRONIZED標記,然後它自動地擷取對象的鎖,調用方法,再釋放鎖。如果發生了異常,線程會自動釋放鎖。
另一方面,同步一個方法塊,繞開JVM內建的對擷取對象鎖和異常處理的支援,這些功能要顯式的寫在位元組碼中。如果你讀過含有同步塊的方法的位元組碼,你將看到更多的額外操作去管理該功能。清單1展示了產生同步方法與同步塊所產生的調用:
package com.geekcap;</p><p>public class SynchronizationExample {<br /> private int i;</p><p> public synchronized int synchronizedMethodGet() {<br /> return i;<br /> }</p><p> public int synchronizedBlockGet() {<br /> synchronized( this ) {<br /> return i;<br /> }<br /> }<br />}
synchronizedMethodGet()方法產生下列位元組碼:
0: aload_0
1: getfield
2: nop
3: iconst_m1
4: ireturn
而下面是synchronizedBlockGet()方法的位元組碼:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: getfield
6: nop
7: iconst_m1
8: aload_1
9: monitorexit
10: ireturn
11: astore_2
12: aload_1
13: monitorexit
14: aload_2
15: athrow
建立同步塊會產生16行位元組碼,然而同步方法只返回5行代碼。
2. ThreadLocal變數
如果你想為一個類的所有執行個體維護單個變數執行個體,你將使用靜態類成員變數來實現這一點。如果你想在每個線程中維護一個變數的執行個體,你將使用thread- local變數。ThreadLocal變數不同於平常的變數,在於每個線程有它自己的變數初始化執行個體,通過get()或set()方法可以訪問這些變數。
讓我們說,你正在開發多線程代碼追蹤器的目的是從你的程式去唯一地標識每個線程的路徑。挑戰在於你需要在跨越多個線程的多個類中協調多個方法。沒有 ThreadLocal,這將是一個很複雜的問題。當一個線程開始執行時,它將產生一個唯一的標記以便於在追蹤器中進行標識,並在在路徑中將這個唯一標記傳給每個方法。
使用ThreadLocal,問題就變得簡單了。線程在啟動並執行開始時初始化thread-local變數,然後在各個類的各個方法中去訪問它,這就能確保該變數只會在當前執行線程中維護路徑資訊。當線程執行完畢時,線程會將它的特定路徑傳遞給一個管理對象,該對象負責維護所有的路徑。
當你需要基於每個線程來儲存變數時,使用ThreadLocal就很有意義。
3. Volatile變數
我估計一大半Java開發員知道Java語言含有關鍵字volatile。其中大約只有10%的人知道它的意義,只有更少的人知道如何高效地使用它。簡言之,將一個變數使用volatile關鍵字進行標識就意味著該變數的值將被不同的線程修改。為了充分理解volatile關鍵字的功用,首先就會協助我們理解線程是如何處理非volatile變數的。
為了改進效能,Java語言規範允許JRE在各個線程中維護一份針對某個變數的引用的複本。你能夠認為這些變數的"thread-local"複本類似於緩衝,這會協助線程避免在每次需要訪問該變數的值時都去檢查主記憶體。
但考慮下面情境可能會發生的事情:兩個線程都啟動了,第一個線程讀到變數A的值為5,而第二個線程讀到變數A的值為10。如果變數A已經從5變到10了,然後第一個線程並不會意識到這一變化,所以它會得到A的錯誤值。如果變數A被標記為volatile,然後在任何時候,某個線程讀取A的值時,它都將查詢
A的主複本並讀到它的當前值。
如果應用中的變數不會改變,那麼使用一個thread-local緩衝將是有意義的。另外,知道volatile關鍵字能為你做些什麼也是很有協助的。
4. volatile比之於同步
如果變數被聲明為volatile,就意味著它會被多個線程所修改。很自然地,你會希望JRE能為volatile變數以某種方式強制執行同步。幸運地是,當訪問volatile變數時,JRE隱式地提供了同步,但會伴隨一個很大的代價:讀volatile變數是同步的,寫volatile變數也是同步的,但非原子性操作不能怎麼做。
這就意味著下面的代碼不是安全執行緒的:
myVolatileVar++;
int temp = 0;<br />synchronize( myVolatileVar ) {<br /> temp = myVolatileVar;<br />}</p><p>temp++;</p><p>synchronize( myVolatileVar ) {<br /> myVolatileVar = temp;<br />}
換言之,如果一個volatile變數按上述方法來進行更新,即先讀取值,並修改之,然後再賦值,在兩個同步操作之間,這個結果是非安全執行緒的。你可以考慮是使用同步,還是依賴JRE對volatile變數的自動同步。更好的方法是根據你的用例:如果賦給volatile變數的值依靠於它的當前值(例如加法操作),如果你想操作是安全執行緒的,那就必須使用同步。
5. 原子欄位更新器
當在多線程環境中加或減一個未經處理資料類型時,使用java.util.concurrent包中新添加的原子類會比編寫你自己的同步代碼塊要好得多。原子類保證能以安全執行緒的方式來執行這些操作,如加減數值,更新值,以及添加值。原子類包括 AtomicInteger,AtomicBoolean,AtomicLong,AtomicLong等等。
使用原子類的挑戰在於所有的類方法,包括get,set,以及get-set方法簇都是原子化的。這就意味著read和write操作不會以同步的方式來修改原子變數的值,也不僅僅重要的讀-更新-寫操作。如果你想對同步代碼的發布能有更好的控制,解決方案就是使用原子欄位更新器。
使用原子更新
原子欄位更新器,如AtomicIntegerFieldUpdater,AtomicLongFieldUpdater和 AtomicReferenceFieldUpdater,是用於volatile欄位的基本封裝器類。在JDK的內部,Java類庫就在使用這些原子類。但在應用程式中,它們還未被廣泛使用,你也沒有理由不使用它們。
清單2展示的樣本,是一個類使用原子更新來改變某人正在閱讀的書:
package com.geeckap.atomicexample;</p><p>public class Book<br />{<br /> private String name;</p><p> public Book()<br /> {<br /> }</p><p> public Book( String name )<br /> {<br /> this.name = name;<br /> }</p><p> public String getName()<br /> {<br /> return name;<br /> }</p><p> public void setName( String name )<br /> {<br /> this.name = name;<br /> }<br />}
清單3中的MyObject類揭露了whatImReading屬性就是你所期望的,該屬性有get和set方法,但set方法做的一些事情不太一樣。不同於簡單地將內部的Book引用賦予一個特定的Book對象(使用清單3中被注釋的代碼就可以做到這一點),該樣本使用了一個
AtomicReferenceFieldUpdater。
清單3
package com.geeckap.atomicexample;</p><p>import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;</p><p>/**<br /> *<br /> * @author shaines<br /> */<br />public class MyObject<br />{<br /> private volatile Book whatImReading;</p><p> private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =<br /> AtomicReferenceFieldUpdater.newUpdater(<br /> MyObject.class, Book.class, "whatImReading" );</p><p> public Book getWhatImReading()<br /> {<br /> return whatImReading;<br /> }</p><p> public void setWhatImReading( Book whatImReading )<br /> {<br /> //this.whatImReading = whatImReading;<br /> updater.compareAndSet( this, this.whatImReading, whatImReading );<br /> }<br />}
AtomicReferenceFieldUpdater
Javadoc對AtomicReferenceFieldUpdater有如下定義:
一個基於反射的工具類,它能對指定類的指定的volatile引用欄位進行原子更新。該類被設計用於原子資料結構,在這種結構中,相同節點的多個引用欄位會進行獨立地原子更新。
在清單3中,通過調用AtomicReferenceFieldUpdater的靜態方法newUpdater就能建立它的執行個體,該方法要接收三個參數:
包含該欄位的對象的類(在這個例子中,就是MyObject)
將被自動更新的對象的類
將被自動更新的欄位的名稱
在執行getWhatImReading方法擷取實際值時沒有使用任何形式的同步,然而setWhatImReading方法的執行則是一個原子操作。
清單4證明了如何去使用setWhatImReading()方法,以及如何判斷變數的值進行了正確地修改:
package com.geeckap.atomicexample;</p><p>import org.junit.Assert;<br />import org.junit.Before;<br />import org.junit.Test;</p><p>public class AtomicExampleTest<br />{<br /> private MyObject obj;</p><p> @Before<br /> public void setUp()<br /> {<br /> obj = new MyObject();<br /> obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );<br /> }</p><p> @Test<br /> public void testUpdate()<br /> {<br /> obj.setWhatImReading( new Book(<br /> "Pro Java EE 5 Performance Management and Optimization" ) );<br /> Assert.assertEquals( "Incorrect book name",<br /> "Pro Java EE 5 Performance Management and Optimization",<br /> obj.getWhatImReading().getName() );<br /> }</p><p>}
查看資源以學習更多關於原子類的知識。
結論
多線程編程總是存在著挑戰性,但涉及到Java平台,它已經獲得了支援去簡化一些多線程編程任務。在本文中,我討論了你在基於Java平台編寫多線程應用時可能不知道的五件事情,包括同步方法與同步塊的不同之處,使用ThreadLocal變數為每個線程去儲存值,針對volatile關鍵字的廣泛誤解
(包括在需要同步時依賴volatile所產生的危險),還簡要地看了一下原子類的複雜之處。查看資源以學習到更多相關知識。