你所不知道的五件事情--多線程編程
這是IBM developerWorks中5 things系列文章中的一篇,講述了關(guān)于多線程的一些應(yīng)用竅門,值得大家學(xué)習(xí)。(2010.11.21最后更新)
摘要:多線程編程不輕松,但它確實(shí)能幫助理解JVM如何細(xì)微地處理不同代碼結(jié)構(gòu)。Steven Haines將分享的5個(gè)竅門會幫助你在處理同步方法,volatile變量以及原子類時(shí)做出更為合理的決定。
盡管很少有Java開發(fā)者能夠忽略多線程編程,且Java平臺類庫支持它,甚至于更少的開發(fā)者能有時(shí)間去深入學(xué)習(xí)線程。相反,我們只是泛泛地學(xué)習(xí)線程,如果需要的話,會向我們的工具箱中添加新的技巧和技術(shù)。通過這種方法你可能會構(gòu)建且運(yùn)行好的應(yīng)用程序,但你還能做得更好。理解Java編譯器和JVM的線程特性,可以幫助你編寫更高效,性能更佳的Java代碼。
在5 things系列的本期文章中,我會介紹一些使用同步方法,volatile變量和原子類等多線程編程的細(xì)節(jié)方面。我的討論特別關(guān)注在這些程序結(jié)構(gòu)是如何與JVM和Java編譯器進(jìn)行交互的,以及不同的交互是如何影響Java應(yīng)用程序性能的。
1. 同步方法與同步塊
你偶爾會衡量是否同步整個(gè)方法調(diào)用,或者只是同步方法中線程完全的子塊。在這種情況下,知道Java編譯器在何時(shí)將源代碼轉(zhuǎn)化為字節(jié)碼是有幫助的,它在處理同步方法和同步塊時(shí)是完全不同的。
當(dāng)JVM在執(zhí)行同步方法時(shí),執(zhí)行線程標(biāo)識方法的method_info結(jié)構(gòu)設(shè)有ACC_SYNCHRONIZED標(biāo)記,然后它自動地獲取對象的鎖,調(diào)用方法,再釋放鎖。如果發(fā)生了異常,線程會自動釋放鎖。
另一方面,同步一個(gè)方法塊,繞開JVM內(nèi)建的對獲取對象鎖和異常處理的支持,這些功能要顯式的寫在字節(jié)碼中。如果你讀過含有同步塊的方法的字節(jié)碼,你將看到更多的額外操作去管理該功能。清單1展示了生成同步方法與同步塊所產(chǎn)生的調(diào)用:
清單1. 兩種同步方法
package com.geekcap;
public class SynchronizationExample {
private int i;
public synchronized int synchronizedMethodGet() {
return i;
}
public int synchronizedBlockGet() {
synchronized( this ) {
return i;
}
}
}
synchronizedMethodGet()方法生成下列字節(jié)碼:
0: aload_0
1: getfield
2: nop
3: iconst_m1
4: ireturn
而下面是synchronizedBlockGet()方法的字節(jié)碼:
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
創(chuàng)建同步塊會產(chǎn)生16行字節(jié)碼,然而同步方法只返回5行代碼。
2. ThreadLocal變量
如果你想為一個(gè)類的所有實(shí)例維護(hù)單個(gè)變量實(shí)例,你將使用靜態(tài)類成員變量來實(shí)現(xiàn)這一點(diǎn)。如果你想在每個(gè)線程中維護(hù)一個(gè)變量的實(shí)例,你將使用thread- local變量。ThreadLocal變量不同于平常的變量,在于每個(gè)線程有它自己的變量初始化實(shí)例,通過get()或set()方法可以訪問這些變量。
讓我們說,你正在開發(fā)多線程代碼追蹤器的目的是從你的程序去唯一地標(biāo)識每個(gè)線程的路徑。挑戰(zhàn)在于你需要在跨越多個(gè)線程的多個(gè)類中協(xié)調(diào)多個(gè)方法。沒有 ThreadLocal,這將是一個(gè)很復(fù)雜的問題。當(dāng)一個(gè)線程開始執(zhí)行時(shí),它將生成一個(gè)唯一的標(biāo)記以便于在追蹤器中進(jìn)行標(biāo)識,并在在路徑中將這個(gè)唯一標(biāo)記傳給每個(gè)方法。
使用ThreadLocal,問題就變得簡單了。線程在運(yùn)行的開始時(shí)初始化thread-local變量,然后在各個(gè)類的各個(gè)方法中去訪問它,這就能確保該變量只會在當(dāng)前執(zhí)行線程中維護(hù)路徑信息。當(dāng)線程執(zhí)行完畢時(shí),線程會將它的特定路徑傳遞給一個(gè)管理對象,該對象負(fù)責(zé)維護(hù)所有的路徑。
當(dāng)你需要基于每個(gè)線程來存儲變量時(shí),使用ThreadLocal就很有意義。
3. Volatile變量
我估計(jì)一大半Java開發(fā)員知道Java語言含有關(guān)鍵字volatile。其中大約只有10%的人知道它的意義,只有更少的人知道如何高效地使用它。簡言之,將一個(gè)變量使用volatile關(guān)鍵字進(jìn)行標(biāo)識就意味著該變量的值將被不同的線程修改。為了充分理解volatile關(guān)鍵字的功用,首先就會幫助我們理解線程是如何處理非volatile變量的。
為了改進(jìn)性能,Java語言規(guī)范允許JRE在各個(gè)線程中維護(hù)一份針對某個(gè)變量的引用的復(fù)本。你能夠認(rèn)為這些變量的"thread-local"復(fù)本類似于緩存,這會幫助線程避免在每次需要訪問該變量的值時(shí)都去檢查主內(nèi)存。
但考慮下面場景可能會發(fā)生的事情:兩個(gè)線程都啟動了,第一個(gè)線程讀到變量A的值為5,而第二個(gè)線程讀到變量A的值為10。如果變量A已經(jīng)從5變到10了,然后第一個(gè)線程并不會意識到這一變化,所以它會得到A的錯(cuò)誤值。如果變量A被標(biāo)記為volatile,然后在任何時(shí)候,某個(gè)線程讀取A的值時(shí),它都將查詢 A的主復(fù)本并讀到它的當(dāng)前值。
如果應(yīng)用中的變量不會改變,那么使用一個(gè)thread-local緩存將是有意義的。另外,知道volatile關(guān)鍵字能為你做些什么也是很有幫助的。
4. volatile比之于同步
如果變量被聲明為volatile,就意味著它會被多個(gè)線程所修改。很自然地,你會希望JRE能為volatile變量以某種方式強(qiáng)制執(zhí)行同步。幸運(yùn)地是,當(dāng)訪問volatile變量時(shí),JRE隱式地提供了同步,但會伴隨一個(gè)很大的代價(jià):讀volatile變量是同步的,寫volatile變量也是同步的,但非原子性操作不能怎么做。
這就意味著下面的代碼不是線程安全的:
myVolatileVar++;
前面的語句可以寫成如下形式:
int temp = 0;
synchronize( myVolatileVar ) {
temp = myVolatileVar;
}
temp++;
synchronize( myVolatileVar ) {
myVolatileVar = temp;
}
換言之,如果一個(gè)volatile變量按上述方法來進(jìn)行更新,即先讀取值,并修改之,然后再賦值,在兩個(gè)同步操作之間,這個(gè)結(jié)果是非線程安全的。你可以考慮是使用同步,還是依賴JRE對volatile變量的自動同步。更好的方法是根據(jù)你的用例:如果賦給volatile變量的值依靠于它的當(dāng)前值(例如加法操作),如果你想操作是線程安全的,那就必須使用同步。
5. 原子字段更新器
當(dāng)在多線程環(huán)境中加或減一個(gè)原始數(shù)據(jù)類型時(shí),使用java.util.concurrent包中新添加的原子類會比編寫你自己的同步代碼塊要好得多。原子類保證能以線程安全的方式來執(zhí)行這些操作,如加減數(shù)值,更新值,以及添加值。原子類包括 AtomicInteger,AtomicBoolean,AtomicLong,AtomicLong等等。
使用原子類的挑戰(zhàn)在于所有的類方法,包括get,set,以及get-set方法簇都是原子化的。這就意味著read和write操作不會以同步的方式來修改原子變量的值,也不僅僅重要的讀-更新-寫操作。如果你想對同步代碼的發(fā)布能有更好的控制,解決方法就是使用原子字段更新器。
使用原子更新
原子字段更新器,如AtomicIntegerFieldUpdater,AtomicLongFieldUpdater和 AtomicReferenceFieldUpdater,是用于volatile字段的基本包裝器類。在JDK的內(nèi)部,Java類庫就在使用這些原子類。但在應(yīng)用程序中,它們還未被廣泛使用,你也沒有理由不使用它們。
清單2展示的示例,是一個(gè)類使用原子更新來改變某人正在閱讀的書:
清單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類只是一個(gè)POJO(Plain Old Java Object),只有一個(gè)字段: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屬性就是你所期望的,該屬性有g(shù)et和set方法,但set方法做的一些事情不太一樣。不同于簡單地將內(nèi)部的Book引用賦予一個(gè)特定的Book對象(使用清單3中被注釋的代碼就可以做到這一點(diǎn)),該示例使用了一個(gè) AtomicReferenceFieldUpdater。
AtomicReferenceFieldUpdater
Javadoc對AtomicReferenceFieldUpdater有如下定義:
一個(gè)基于反射的工具類,它能對指定類的指定的volatile引用字段進(jìn)行原子更新。該類被設(shè)計(jì)用于原子數(shù)據(jù)結(jié)構(gòu),在這種結(jié)構(gòu)中,相同節(jié)點(diǎn)的多個(gè)引用字段會進(jìn)行獨(dú)立地原子更新。
在清單3中,通過調(diào)用AtomicReferenceFieldUpdater的靜態(tài)方法newUpdater就能創(chuàng)建它的實(shí)例,該方法要接收三個(gè)參數(shù):
包含該字段的對象的類(在這個(gè)例子中,就是MyObject)
將被自動更新的對象的類
將被自動更新的字段的名稱
在執(zhí)行g(shù)etWhatImReading方法獲取實(shí)際值時(shí)沒有使用任何形式的同步,然而setWhatImReading方法的執(zhí)行則是一個(gè)原子操作。
清單4證明了如何去使用setWhatImReading()方法,以及如何判斷變量的值進(jìn)行了正確地修改:
清單4. 練習(xí)原子更新的測試用例
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() );
}
}
查看資源以學(xué)習(xí)更多關(guān)于原子類的知識。
結(jié)論
多線程編程總是存在著挑戰(zhàn)性,但涉及到Java平臺,它已經(jīng)獲得了支持去簡化一些多線程編程任務(wù)。在本文中,我討論了你在基于Java平臺編寫多線程應(yīng)用時(shí)可能不知道的五件事情,包括同步方法與同步塊的不同之處,使用ThreadLocal變量為每個(gè)線程去存儲值,針對volatile關(guān)鍵字的廣泛誤解 (包括在需要同步時(shí)依賴volatile所產(chǎn)生的危險(xiǎn)),還簡要地看了一下原子類的復(fù)雜之處。查看資源以學(xué)習(xí)到更多相關(guān)知識。