你所不知道的五件事情--多線程編程
這是IBM developerWorks中5 things系列文章中的一篇,講述了關于多線程的一些應用竅門,值得大家學習。(2010.11.22最后更新)
摘要:多線程編程不輕松,但它確實能幫助理解JVM如何細微地處理不同代碼結構。Steven Haines將分享的5個竅門會幫助你在處理同步方法,volatile變量以及原子類時做出更為合理的決定。
盡管很少有Java開發者能夠忽略多線程編程,且Java平臺類庫支持它,甚至于更少的開發者能有時間去深入學習線程。相反,我們只是泛泛地學習線程,如果需要的話,會向我們的工具箱中添加新的技巧和技術。通過這種方法你可能會構建且運行好的應用程序,但你還能做得更好。理解Java編譯器和JVM的線程特性,可以幫助你編寫更高效,性能更佳的Java代碼。
在5 things系列的本期文章中,我會介紹一些使用同步方法,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_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;
synchronize( myVolatileVar ) {
temp = myVolatileVar;
}
temp++;
synchronize( myVolatileVar ) {
myVolatileVar = temp;
}
換言之,如果一個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展示的示例,是一個類使用原子更新來改變某人正在閱讀的書:
清單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(Plain Old Java Object),只有一個字段: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類揭露了whatImReading屬性就是你所期望的,該屬性有get和set方法,但set方法做的一些事情不太一樣。不同于簡單地將內部的Book引用賦予一個特定的Book對象(使用清單3中被注釋的代碼就可以做到這一點),該示例使用了一個 AtomicReferenceFieldUpdater。
AtomicReferenceFieldUpdater
Javadoc對AtomicReferenceFieldUpdater有如下定義:
一個基于反射的工具類,它能對指定類的指定的volatile引用字段進行原子更新。該類被設計用于原子數據結構,在這種結構中,相同節點的多個引用字段會進行獨立地原子更新。
在清單3中,通過調用AtomicReferenceFieldUpdater的靜態方法newUpdater就能創建它的實例,該方法要接收三個參數:
包含該字段的對象的類(在這個例子中,就是MyObject)
將被自動更新的對象的類
將被自動更新的字段的名稱
在執行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;
@Before
public void setUp()
{
obj = new MyObject();
obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
}
@Test
public 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平臺編寫多線程應用時可能不知道的五件事情,包括同步方法與同步塊的不同之處,使用ThreadLocal變量為每個線程去存儲值,針對volatile關鍵字的廣泛誤解 (包括在需要同步時依賴volatile所產生的危險),還簡要地看了一下原子類的復雜之處。查看資源以學習到更多相關知識。