Java Tutorials -- Concurrency
近一段時(shí)間在使用Thinking in Java(4th, English)和Java Concurrency in Practice學(xué)習(xí)Java并發(fā)編程。不得不說官方的Java Tutorias是很好的Java并發(fā)編程入門級(jí)教程,故將它其中的Concurrency一章翻譯在了此處。與我翻譯Java Tutorias中Generics一章時(shí)的目的相同,只是對(duì)自己近一段時(shí)間學(xué)習(xí)的回顧罷了,也希望對(duì)其它朋友能有所助益。(2007.11.29最后更新)課程: 并發(fā)
計(jì)算機(jī)用戶們將他們的系統(tǒng)能夠在同一時(shí)刻做一件以上的事情視為一種當(dāng)然。他們猜想著,他們?cè)谑褂米痔幚砥鞯耐瑫r(shí)其它的應(yīng)用程序正在下載文件,管理打印隊(duì)列和傳輸音頻流。例如,音頻流應(yīng)用程序必須在同一時(shí)刻從網(wǎng)絡(luò)上讀取數(shù)字音頻數(shù)據(jù),解壓它們,管理重放功能,并更新它們的顯示方式。
Java平臺(tái)徹底地被設(shè)計(jì)為支持并發(fā)的編程,它有著在Java程序設(shè)計(jì)語言和Java字節(jié)類庫中的基本并發(fā)支持。從5.0開始,Java平臺(tái)也將包含著高級(jí)的并發(fā)API。該課程介紹了該平臺(tái)的基本并發(fā)編程支持,并概述了java.util.concurrent包中的一些高級(jí)API。
進(jìn)程與線程
在并發(fā)編程中,有兩種基本的執(zhí)行單元:進(jìn)程和線程。在Java程序設(shè)計(jì)語言中,并發(fā)編程幾乎只關(guān)心線程。當(dāng)然,進(jìn)程也是重要的。
計(jì)算機(jī)系統(tǒng)一般都有很多活躍的進(jìn)程與線程。甚至在只有一個(gè)執(zhí)行內(nèi)核的系統(tǒng)中也是如此。在一個(gè)給定的時(shí)刻,確實(shí)只能有一個(gè)線程是在執(zhí)行。通過一種稱之為"分片"的操作系統(tǒng)特性,進(jìn)程與線程共享著單核的處理時(shí)間。
計(jì)算機(jī)系統(tǒng)擁有多個(gè)處理器或有多個(gè)執(zhí)行內(nèi)核的單處理器正在變得越來越普遍。這將很大地提升系統(tǒng)在處理進(jìn)程與線程的并發(fā)執(zhí)行時(shí)的能力。
進(jìn)程
一個(gè)進(jìn)程擁有一個(gè)自我包括的執(zhí)行環(huán)境。一個(gè)進(jìn)程一般擁有一個(gè)完整的,內(nèi)置的基本運(yùn)行時(shí)資源;特別地,每個(gè)進(jìn)程都擁有它自己的內(nèi)存空間。
進(jìn)程經(jīng)常被視為程序或應(yīng)用的同義詞。然而,用戶所看到的單個(gè)應(yīng)用可能實(shí)際上是一組相互協(xié)作的進(jìn)程。為了便于進(jìn)程之間的通信,大多數(shù)操作系統(tǒng)支持內(nèi)部進(jìn)程通信(Inter Process Communication, IPC),例如管道與套接字。IPC不僅被用于同一個(gè)系統(tǒng)中進(jìn)程之間的通信,還可以處理不同系統(tǒng)中進(jìn)程之間的通信。
Java虛擬機(jī)的大多數(shù)實(shí)現(xiàn)都是作為單個(gè)進(jìn)程的。一個(gè)Java應(yīng)用程序可以使用ProcessBuilder對(duì)象來創(chuàng)建額外的進(jìn)程。多進(jìn)程應(yīng)用程序已經(jīng)超出了本課程的范圍。
線程
線程有時(shí)候被稱之為輕量級(jí)進(jìn)程。進(jìn)程和線程都能提供一個(gè)執(zhí)行環(huán)境,但創(chuàng)建一個(gè)線程所需要的資源少于創(chuàng)建一個(gè)進(jìn)程。
線程存在于一個(gè)進(jìn)程中--每個(gè)進(jìn)程至少有一個(gè)線程。線程分享進(jìn)程的資源,包括內(nèi)存和被打開的文件。這樣做是為了高效,但在通信方面有著潛在的問題。
多線程執(zhí)行是Java平臺(tái)的一個(gè)本質(zhì)特性。每個(gè)應(yīng)用程序至少擁有一個(gè)線程--或者說是多個(gè),如果你把像內(nèi)存管理和信號(hào)處理這樣的系統(tǒng)進(jìn)程算進(jìn)來的話。但從應(yīng)用程序員的角度來看,你僅以一個(gè)線程開始,該線程被稱之為主線程。該線程擁有創(chuàng)建其它線程的能力,我們將在下一節(jié)中使用例子證明這一點(diǎn)。
線程對(duì)象
每個(gè)線程都關(guān)聯(lián)著一個(gè)Thread類的實(shí)例。為了創(chuàng)建并發(fā)應(yīng)用,有兩種使用Thread對(duì)象的基本策略。
* 為了直接的控制線程的創(chuàng)建與管理,當(dāng)應(yīng)用每次需要啟動(dòng)一個(gè)同步任務(wù)時(shí)就實(shí)例化一個(gè)Thread類。
* 從你的應(yīng)用中將線程管理抽象出來,將應(yīng)用的任務(wù)傳遞給一個(gè)執(zhí)行器(Executor)。
本節(jié)將描述Thread對(duì)象的使用,執(zhí)行器將與其它的高級(jí)并發(fā)對(duì)象們一起討論。
定義與啟動(dòng)一個(gè)線程
一個(gè)應(yīng)用要?jiǎng)?chuàng)建一個(gè)Thread的實(shí)例就必須提供那些要在該線程中運(yùn)行的代碼。有兩種方法去做這些:
* 提供一個(gè)Runnable對(duì)象。Runnable接口只定義了一個(gè)方法--run,它要包含那些將在該線程中執(zhí)行的代碼。Runnable對(duì)象將被傳入Thread的構(gòu)造器,如例子HelloRunnable如示:
public class HelloRunnable implements Runnable {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new Thread(new HelloRunnable())).start();
}
}
* 繼承Thread類。Thread類本身也實(shí)現(xiàn)了Runnable接口,可是它的run方法什么都沒有做。一個(gè)應(yīng)用可以繼承Thread類,并提供它自己的run方法實(shí)現(xiàn),就如HelloThread類所做的:
public class HelloThread extends Thread {
public void run() {
System.out.println("Hello from a thread!");
}
public static void main(String args[]) {
(new HelloThread()).start();
}
}
注意,為了啟動(dòng)新的線程,這兩個(gè)例子都調(diào)用了Thread.start方法。
你應(yīng)該使用哪一種方式呢?第一種方式使用了Runnable對(duì)象,這更加通用,因?yàn)镽unnable對(duì)象還可以繼承Thread之外的其它類。第二種方式可以很方便的使用在簡(jiǎn)單應(yīng)用中,但這樣的話,你的任務(wù)類必須被限制為Thread的一個(gè)子類。本課程關(guān)注于第一種方式,該方法將Runnable任務(wù)從執(zhí)行該任務(wù)的Thread對(duì)象中分離出來。這種方法不僅更富彈性,而且它還適用于后面將要提到的高級(jí)線程管理API中。
Thread類為線程的管理定義了一組很有有的方法。它們包括一些能夠提供關(guān)于該線程信息的靜態(tài)方法,以及當(dāng)該線程調(diào)用這些方法時(shí)能夠影響到該線程的狀態(tài)。另一些方法則是被管理該線程和Thread對(duì)象的其它的線程調(diào)用。
使用sleep方法暫停執(zhí)行
Thread.sleep方法使當(dāng)前運(yùn)行的線程暫停執(zhí)行一段指定的時(shí)間周期。這是使相同應(yīng)用中的其它線程,或是相同計(jì)算機(jī)系統(tǒng)中的其它應(yīng)用能夠獲得處理器時(shí)間的有效方法。sleep方法也被用于"緩步",如接下來的例子所示,并且等待其它負(fù)有任務(wù)的線程,這些線程被認(rèn)為有時(shí)間需求。
sleep方法有兩個(gè)相互重載的版本:一個(gè)指定了睡眠時(shí)間的毫秒數(shù);另一個(gè)指定了睡眠的納秒數(shù)。然而,這些睡眠時(shí)間都無法得到精確的保證,因?yàn)槭艿讓硬僮飨到y(tǒng)所提供的機(jī)制的限制。而且,睡眠周期會(huì)由于中斷而被停止,我們將在下面的章節(jié)中看到。在任何情況下,你都不能猜想調(diào)用sleep方法后都能精確地在指定時(shí)期周期內(nèi)暫停線程。
示例SeelpMessages使用sleep方法在每4秒的間隔內(nèi)打印信息:
public class SleepMessages {
public static void main(String args[]) throws InterruptedException {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
for (int i = 0; i < importantInfo.length; i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
System.out.println(importantInfo[i]);
}
}
}
注意main方法聲明了它會(huì)拋出InterruptedException異常。當(dāng)其它的線程中斷了正處于睡眠的當(dāng)前線程時(shí),sleep方法就會(huì)拋出該異常。由于該應(yīng)用并沒有定義其它的線程去造成中斷,所以它沒必要去捕獲InterruptedException。
中斷
中斷指明了一個(gè)線程應(yīng)該停止它正在做的事情并做些其它的事情。它使程序員要仔細(xì)地考慮讓一個(gè)線程如何回應(yīng)中斷,但十分普通的做法是讓這個(gè)線程停終止。這是本課程特別強(qiáng)調(diào)的用法。
一個(gè)線程以調(diào)用Thread對(duì)象中的interrupt方法的方式將中斷信號(hào)傳遞給另一個(gè)線程而使它中斷。為了使中斷機(jī)制能正確地工作,被中斷的線程必須支持它自己的中斷。
支持中斷
一個(gè)線程如何支持它自己的中斷呢?這取決于它當(dāng)前正在干什么?如果該線程正在調(diào)用那些會(huì)拋出InterruptedException的方法,那么當(dāng)它捕獲了該異常之后就只會(huì)從run方法內(nèi)返回。例如,假設(shè)SleepMessages示例中打印信息的循環(huán)就在該線程的Runnable對(duì)象的run方法中。再做如下修改,使它能夠中斷:
for (int i = 0; i < importantInfo.length; i++) {
//Pause for 4 seconds
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
//We've been interrupted: no more messages.
return;
}
//Print a message
System.out.println(importantInfo[i]);
}
許多會(huì)拋出InterruptedException異常的方法,如sleep,都被設(shè)計(jì)成當(dāng)它們收到一個(gè)中斷時(shí)就取消它們當(dāng)前的操作并立即返回。
如果該線程長(zhǎng)時(shí)間運(yùn)行且沒有調(diào)用會(huì)拋出InterruptedException異常的方法,那又會(huì)怎樣呢?它必須周期性地調(diào)用Thread.interrupted方法,如果收到了一個(gè)中斷,該方法將返回true。例如:
for (int i = 0; i < inputs.length; i++) {
heavyCrunch(inputs[i]);
if (Thread.interrupted()) {
//We've been interrupted: no more crunching.
return;
}
}
在這個(gè)簡(jiǎn)單的例子中,這些代碼僅是簡(jiǎn)單地測(cè)試該線程是否收到了中斷,如果收到了就退出。在更復(fù)雜的例子中,它可能為了更有意義些而拋出一個(gè)InterruptedException異常:
if (Thread.interrupted()) {
throw new InterruptedException();
}
這就能使處理中斷的代碼被集中在catch語句塊中。
中斷狀態(tài)標(biāo)志
被認(rèn)為是中斷狀態(tài)的內(nèi)部標(biāo)志用于中斷機(jī)制的實(shí)現(xiàn)。調(diào)用Thread.interrupt方法會(huì)設(shè)置這個(gè)標(biāo)記。當(dāng)一個(gè)線程調(diào)用靜態(tài)方法Thread.interrupted去檢查中斷時(shí),中斷狀態(tài)就被清理了。用于一個(gè)線程查詢另一個(gè)線程中斷狀態(tài)的非靜態(tài)方法Thread.isInterrupted不會(huì)改變中斷狀態(tài)標(biāo)志。
按照慣例,任何通過拋出InterruptedException異常而退出的方法都會(huì)在它退出時(shí)清理中斷狀態(tài)。然而,總存在著這樣的可能性,中斷狀態(tài)會(huì)由于其它線程調(diào)用interrupt方法而立即被再次設(shè)置。
Joins
join允許一個(gè)線程等待另一個(gè)線程完成。如果Thread對(duì)象t的線程當(dāng)前正在執(zhí)行,
t.join();
上述語句將導(dǎo)致當(dāng)前線程暫停執(zhí)行只到t的線程終止為止。join的一個(gè)重載版本允許程序員指定等待的周期。然而,與sleep方法一樣,join的時(shí)長(zhǎng)依賴于操作系統(tǒng),所以你不應(yīng)該設(shè)想join將準(zhǔn)確地等待你所指定的時(shí)長(zhǎng)。
像sleep方法一樣,在由于InterruptedException異常而退出時(shí),join方法也要應(yīng)對(duì)中斷。
SimpleThreads示例
下面的例子匯集了本節(jié)一些概念。SimpleThreads類由兩個(gè)線程組成。第一個(gè)線程就是每個(gè)Java應(yīng)用都有的主線程。主線程從一個(gè)Runnable對(duì)象,MessageLoop,創(chuàng)建一個(gè)新的線程,并等待它結(jié)束。如果MessageLoop線程花的時(shí)間太長(zhǎng)了,主線程就會(huì)中斷它。
MessageLoop線程打印出一系列的信息。如果在打印出所以信息之前就被中斷了,MessageLoop線程將會(huì)打印一條信息并退出。
public class SimpleThreads {
//Display a message, preceded by the name of the current thread
static void threadMessage(String message) {
String threadName = Thread.currentThread().getName();
System.out.format("%s: %s%n", threadName, message);
}
private static class MessageLoop implements Runnable {
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
try {
for (int i = 0; i < importantInfo.length; i++) {
//Pause for 4 seconds
Thread.sleep(4000);
//Print a message
threadMessage(importantInfo[i]);
}
} catch (InterruptedException e) {
threadMessage("I wasn't done!");
}
}
}
public static void main(String args[]) throws InterruptedException {
//Delay, in milliseconds before we interrupt MessageLoop
//thread (default one hour).
long patience = 1000 * 60 * 60;
//If command line argument present, gives patience in seconds.
if (args.length > 0) {
try {
patience = Long.parseLong(args[0]) * 1000;
} catch (NumberFormatException e) {
System.err.println("Argument must be an integer.");
System.exit(1);
}
}
threadMessage("Starting MessageLoop thread");
long startTime = System.currentTimeMillis();
Thread t = new Thread(new MessageLoop());
t.start();
threadMessage("Waiting for MessageLoop thread to finish");
//loop until MessageLoop thread exits
while (t.isAlive()) {
threadMessage("Still waiting...");
//Wait maximum of 1 second for MessageLoop thread to
//finish.
t.join(1000);
if (((System.currentTimeMillis() - startTime) > patience) &&
t.isAlive()) {
threadMessage("Tired of waiting!");
t.interrupt();
//Shouldn't be long now -- wait indefinitely
t.join();
}
}
threadMessage("Finally!");
}
}
同步
線程通信主要是通過訪問共享的字段以及這些字段所涉及的對(duì)象引用。這種通信的形式十分的高效,但它可能造成兩種錯(cuò)誤:線程干涉和內(nèi)存一致性錯(cuò)誤。用于阻止這些錯(cuò)誤的工具就是同步。
* 線程干預(yù)介紹了當(dāng)多個(gè)線程訪問共享數(shù)據(jù)時(shí)產(chǎn)生的錯(cuò)誤。
* 內(nèi)存一致性錯(cuò)誤介紹了指由對(duì)共享內(nèi)存不一致的查看而導(dǎo)致的錯(cuò)誤。
* 同步方法介紹了一種能夠有效地防止線程干預(yù)和內(nèi)存一致性錯(cuò)誤的常用方法。
* 隱含鎖和同步介紹了一種更通用的同步方法,并介紹了同步是如何基于隱含鎖的。
* 原子訪問介紹這種通用的不會(huì)受其它線程干預(yù)的操作概念。
線程干預(yù)
考慮這個(gè)叫Counter的簡(jiǎn)單類
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
Counter被設(shè)計(jì)成讓每次調(diào)用increment方法后c就加1,而每次調(diào)用decrement方法后c就減1。然而,如果一個(gè)Counter對(duì)象被多個(gè)線程所引用,那么線程之前的干預(yù)可能不會(huì)使所期望的事情發(fā)生。
當(dāng)在不同線程中的兩個(gè)操作交叉地作用于同一數(shù)據(jù)時(shí),干預(yù)就發(fā)生了。這就是說兩個(gè)操作由多步組成,并且步調(diào)之間相互重疊。
看起來作用于Counter實(shí)例的操作不可能是交叉的,因?yàn)檫@兩個(gè)關(guān)于c變量的操作都是一元的簡(jiǎn)單語句。可是,如此簡(jiǎn)單的語句也能夠被虛擬機(jī)解釋成多個(gè)步驟。我們不用檢查虛擬機(jī)所做的特定步驟--我們足以知道一元表達(dá)式c++可被分解成如下三步:
1. 獲取c的當(dāng)前值。
2. 將這個(gè)被取出的值加1。
3. 將被加的值再放回c變量中。
表達(dá)式c--也能被進(jìn)行相同地分解,除了將第二步的加替換為減。
猜想在線程B調(diào)用decrement方法時(shí),線程A調(diào)用了increment方法。如果c的初始值為0,它們交叉的動(dòng)作可能是如下順序:
1. 線程A:取出c。
2. 線程B:取出c。
3. 線程A:將取出的值加1,結(jié)果為1。
4. 線程B:將取出的值減一,結(jié)果為-1。
5. 線程A:將結(jié)果存于c中,c現(xiàn)在為1。
6. 線程B:將結(jié)果存于c中,c現(xiàn)在為-1。
線程A的結(jié)果丟失了,被線程B覆蓋了。這個(gè)特殊的交叉只是一種可能性。在不同的環(huán)境下,可能是線程B的結(jié)果丟失,也可能根本就沒有發(fā)生任何錯(cuò)誤。因?yàn)樗鼈兪遣豢赡茴A(yù)知的,所以很難發(fā)現(xiàn)并修正線程干預(yù)缺陷。
內(nèi)存一致性錯(cuò)誤
當(dāng)不同的線程觀察到本應(yīng)相同但實(shí)際上不同的數(shù)據(jù)時(shí),內(nèi)存一致性錯(cuò)誤就發(fā)生了。導(dǎo)致內(nèi)存一致性錯(cuò)誤的原因十分復(fù)雜并且超出了本教程的范圍。幸運(yùn)地是,應(yīng)用程序員并不需要了解這些原因的細(xì)節(jié)。所需要的就是一個(gè)避免它們的策略。
避免內(nèi)存一致性錯(cuò)誤的關(guān)鍵就是要理解"happens-before"的關(guān)系。這個(gè)關(guān)系保證了由一個(gè)特定的語句所寫的內(nèi)存對(duì)其它特定的語句都是可見。為了解它,可以考慮下面的例子。假設(shè)一個(gè)簡(jiǎn)單的int型字段的定義與初始化:
int counter = 0;
counter字段被兩個(gè)線程,A和B,共享。假設(shè)線程A增加counter的值:
counter++;
很短的時(shí)間之后,線程B打印出counter的值:
System.out.println(counter);
如果這兩條語句是在同一個(gè)線程中執(zhí)行,那就是可以很肯定地猜測(cè)被打印出的會(huì)是"1"。但如果在不同的線程中執(zhí)行這兩條語句,被打印出的值可能正好是"0",因?yàn)闆]有什么能保證線程A對(duì)counter的改變能被線程B看到--除非應(yīng)用程序員在這兩條語句之間建立了"happens-before"關(guān)系。
有多種方式能夠創(chuàng)建"happens-before"關(guān)系。其中之一就是同步,我們將在接下來的一節(jié)中看到它。
我們已經(jīng)看到了兩種建立"happens-before"關(guān)系的方法。
* 當(dāng)一條語句調(diào)用Thread.start方法,一個(gè)新的線程執(zhí)行的每條語句都有"happens-before"關(guān)系的語句與那些也有著"happens-before"關(guān)系。這些代碼的作用就是使新線程的創(chuàng)建對(duì)于其它的新線程是可見的。
* 當(dāng)一個(gè)線程終止并在另一個(gè)線程中調(diào)用Thread.join導(dǎo)致返回,然后所有的由已終止的線程執(zhí)行的語句伴著隨后成功join的所有語句都有"happens-before"關(guān)系。那么在該線程中的代碼所產(chǎn)生的影響對(duì)于join進(jìn)來的線程就是可見的。
要看創(chuàng)建"happens-before"關(guān)系的一列方法,可以參考java.util.concurrent包的摘要頁面。
同步方法
Java設(shè)計(jì)程序需要提供兩種基本的同步常用法:同步方法和同步語句。其中更為復(fù)雜的一種,同步語句,將在下一節(jié)講述。本節(jié)是關(guān)于同步方法的。
使一個(gè)方法是可同步的,只要簡(jiǎn)單地將關(guān)鍵字synchronized加到它的聲明中:
public class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
如果count是SynchronizedCounter的一個(gè)實(shí)例,那么使這些方法同步將有兩個(gè)作用:
* 第一,對(duì)同一個(gè)對(duì)象中的同步方法進(jìn)行交叉的調(diào)用就不可能了。當(dāng)一個(gè)線程正在調(diào)用一個(gè)對(duì)象中的一個(gè)同步方法時(shí),所有其它的調(diào)用該對(duì)象的同步方法的線程將被阻塞,直到第一個(gè)線程結(jié)束對(duì)該對(duì)象的工作。
* 第二,當(dāng)同步方法存在,它就會(huì)與在同一對(duì)象中后序調(diào)用的方法自動(dòng)地建立"happens-before"關(guān)系。
注意,構(gòu)造器不能是可同步--對(duì)一個(gè)構(gòu)造器使用關(guān)鍵字synchronized是一個(gè)語法錯(cuò)誤。同步構(gòu)造器沒有意義,因?yàn)橹挥幸粋€(gè)線程要?jiǎng)?chuàng)建對(duì)象,當(dāng)它正在被構(gòu)造時(shí)才會(huì)訪問構(gòu)造器。
警告:當(dāng)構(gòu)造一個(gè)將會(huì)在線程之間共享的對(duì)象時(shí),要非常小心對(duì)象的引用過早地"溢出"。例如,假設(shè)你要維護(hù)一個(gè)叫instances的List去包含class的每個(gè)實(shí)例。你可能會(huì)嘗試著加入下面一行
instances.add(this);
到你的構(gòu)造囂。但之后其它的線程可以在這個(gè)對(duì)象構(gòu)造完成之前就可以instances去訪問該對(duì)象。
同步方法使一個(gè)簡(jiǎn)單的防止線程干預(yù)和內(nèi)存一致錯(cuò)誤的策略成為可能:如果一個(gè)對(duì)象對(duì)于一個(gè)以上的線程是可見的,所有針對(duì)該對(duì)象的變量的讀與寫都要通過同步方法。(有一個(gè)很重要的例外:在被構(gòu)造之后就不能被修改的final字段,一旦它被創(chuàng)建,就能夠被非同步方法安全地讀取)。這種策略十分高效,但會(huì)出現(xiàn)活躍度問題,我們將在后面的教程中見到。
內(nèi)部鎖與同步
同步是圍繞著一個(gè)被認(rèn)為是內(nèi)部鎖或監(jiān)視鎖的內(nèi)部實(shí)體而建立(API規(guī)范經(jīng)常就稱這個(gè)實(shí)體為"監(jiān)視器")。內(nèi)部鎖在同步的兩個(gè)方面扮演著角色:強(qiáng)制排他地訪問一個(gè)對(duì)象的狀態(tài),為那些必須是可見的狀態(tài)建立"happen-before"關(guān)系。
每個(gè)對(duì)象都有一個(gè)與之關(guān)聯(lián)的內(nèi)部鎖。一般地,一個(gè)線程要排他并一致地訪問一個(gè)對(duì)象的字段就必須在訪問它之前就獲得這個(gè)對(duì)象的內(nèi)部鎖,在這個(gè)線程使用完之后就釋放這個(gè)內(nèi)部鎖。在獲得鎖與釋放鎖之間的這段時(shí)間內(nèi),這個(gè)線程被認(rèn)為擁有這個(gè)內(nèi)部鎖。一但線程擁有了內(nèi)部鎖,其它的線程就不能再獲得相同的鎖了。當(dāng)另一個(gè)線程試圖獲得這個(gè)鎖時(shí),它將會(huì)被阻塞。
當(dāng)線程釋放了一個(gè)內(nèi)部鎖,在這個(gè)動(dòng)作與后續(xù)想獲得同一個(gè)鎖的動(dòng)作之間的"happens-before"關(guān)系就建立起來了。
在同步方法中的鎖
當(dāng)線程調(diào)用了同步方法,它就自動(dòng)地獲得這個(gè)方法所在對(duì)象的內(nèi)部鎖,當(dāng)這個(gè)方法返回時(shí)它就會(huì)釋放這個(gè)鎖。即使這個(gè)返回是由一個(gè)未捕獲的異常造成的,鎖也會(huì)被釋放。
你可能會(huì)對(duì)調(diào)用一個(gè)靜態(tài)的同步方法時(shí)所發(fā)生的事情感到驚訝,因?yàn)殪o態(tài)方法是與一個(gè)類,而不是一個(gè)對(duì)象,相關(guān)聯(lián)的。在這種情況下,線程要求獲得與這個(gè)類相關(guān)的Class對(duì)象的內(nèi)部鎖。因此訪問類的靜態(tài)字段是被一個(gè)與作用于類的實(shí)例的鎖不同的鎖控制的。
同步語句
創(chuàng)建同步代碼的另一種方式是使用同步語句。與同步方法不同,同步語句必須要指定提供內(nèi)部鎖的對(duì)象:
public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}
在這個(gè)例子中,addName方法要對(duì)lastName和nameCount的修改進(jìn)行同步,但也要避免同步地調(diào)用另一個(gè)對(duì)象中的方法(從同步代碼中調(diào)用另一個(gè)對(duì)象的方法會(huì)產(chǎn)生的問題將在Liveness章節(jié)中講述)。不用同步語句,就只能是一個(gè)隔離的非同步方法,其目的只是為了調(diào)用nameList.add方法。
同步語句對(duì)使用細(xì)致的同步去提高并發(fā)應(yīng)用也是有用的。例如,假設(shè)類MsLunch有兩個(gè)實(shí)例字段,c1和c2,從來都沒有一起被使用過。這些字段的更新都必須是同步的,但沒有道理在交叉地對(duì)c2進(jìn)行更新時(shí)防止對(duì)c1的更新--這樣做會(huì)創(chuàng)建不必要的阻塞而減少并發(fā)。我們創(chuàng)建兩個(gè)對(duì)象單獨(dú)地提供鎖,而不是使用同步方法或反而使用與this關(guān)聯(lián)的鎖。
public class MsLunch {
private long c1 = 0;
private long c2 = 0;
private Object lock1 = new Object();
private Object lock2 = new Object();
public void inc1() {
synchronized(lock1) {
c1++;
}
}
public void inc2() {
synchronized(lock2) {
c2++;
}
}
}
使用這種方法必須極其的小心。你必須非常地肯定交叉地訪問這些受影響的字段是安全的。
可重進(jìn)入的同步
回憶一下,線程不能獲得被其它線程占有的鎖。但線程可以獲得被它自己占有的鎖。允許線程多次獲得相同的鎖就能夠形成可重進(jìn)入的同步。這就能解釋這樣一種情況,當(dāng)同步代碼直接或間接地調(diào)用了一個(gè)已經(jīng)包含同步代碼的方法,但兩組代碼都使用相同的鎖。沒有可重進(jìn)入的同步,同步代碼將不得不采取更多額外的預(yù)防措施去避免線程被自己阻塞。
原子訪問
在編程中,一個(gè)原子操作就是所有有效的動(dòng)作一次性發(fā)生。原子操作不能在中間停止:它要么完全發(fā)生,要么完全不發(fā)生。原子操作不會(huì)有任何可見的副作用,直到該行為完成。
我們已經(jīng)看到了像c++這樣的加法表達(dá)式不是一個(gè)原子操作。非常簡(jiǎn)單的表達(dá)甚至都可以被定義成能被分解為其它操作的復(fù)雜操作。但是,有些操作你可以認(rèn)為它們是原子的:
* 讀和寫引用變量和大多數(shù)基本數(shù)據(jù)類型變量(除long和double之外的其它基本數(shù)據(jù)類型)
* 讀和寫被聲明為volatile的變量(包括long和double型的變量)都是原子的。
原子操作不能被交叉地執(zhí)行,因此使用它們可以不必?fù)?dān)心線程干預(yù)。然而,這并不能完全清除對(duì)原子操作進(jìn)行同步的需要,因?yàn)閮?nèi)存一致性錯(cuò)誤的可能性仍然存在。使用volatile變量可以降低內(nèi)存一致性錯(cuò)誤的風(fēng)險(xiǎn),因?yàn)槿魏吾槍?duì)volatile變量的寫操作都與后續(xù)的針對(duì)該變量的讀操作之間建立了"happen-before"關(guān)系。這就意味著對(duì)一個(gè)voloatile變量的改變對(duì)于其它線程都是可見的。進(jìn)一步說,這也意味著當(dāng)一個(gè)線程讀一個(gè)volatile變量時(shí),它不僅能看到該volatile變量的最新變化,也能看到導(dǎo)致該變化的代碼的副作用。
使簡(jiǎn)潔的原子變量訪問比通過同步代碼訪問這些變量更加高效,但也要求應(yīng)用程序員更加小心以避免內(nèi)存一致性錯(cuò)誤。額外的努力是否值得,取決于應(yīng)用的規(guī)模與復(fù)雜度。
java.util.concurrent包中的一些類提供了一些不依賴于同步的原子方法。我們將在High Level Concurrency Objects一節(jié)中討論它們。
死鎖
死鎖描述了一種兩個(gè)或以上的線程永久地相互等待而被阻塞的情形。這兒就有一個(gè)例子。
Alphonse和Gaston是朋友,并且都很崇尚禮節(jié)。禮節(jié)的一條嚴(yán)格規(guī)則就是,當(dāng)你向朋友鞠躬時(shí),你必須保持鞠躬的姿勢(shì)直到你的朋友能有機(jī)會(huì)向你還以鞠躬。不幸地是,這條規(guī)則沒有說明這樣一種可能性,即兩個(gè)朋友可能在同時(shí)間相互鞠躬。
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s has bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(new Runnable() {
public void run() { alphonse.bow(gaston); }
}).start();
new Thread(new Runnable() {
public void run() { gaston.bow(alphonse); }
}).start();
}
}
當(dāng)Deadlock運(yùn)行后,極有可能當(dāng)兩個(gè)線程試圖調(diào)用bowBack時(shí)它們被阻塞了。沒有一種阻塞會(huì)結(jié)束,因?yàn)槊總€(gè)線程都在另一方退出bow方法。
饑餓與活性鎖
饑餓與活性鎖是沒有死鎖那么普遍的問題,也仍然是每個(gè)并發(fā)軟件的設(shè)計(jì)者都可能遇到的問題。
饑餓
饑餓所描述的情形是指當(dāng)一個(gè)線程不能正常地訪問到共享資源,也就不能得到進(jìn)步。當(dāng)共享資源被"貪婪"的線程長(zhǎng)時(shí)間占有時(shí),這種情況就會(huì)發(fā)生。例如,假設(shè)一個(gè)對(duì)象提供了一個(gè)經(jīng)常會(huì)耗費(fèi)很長(zhǎng)時(shí)間才會(huì)返回的同步方法。如果一個(gè)線程頻繁地調(diào)用這個(gè)方法,其它也需要頻繁地同步訪問相同對(duì)象的線程就會(huì)經(jīng)常被阻塞。
活性鎖
一個(gè)線程的行為經(jīng)常是對(duì)另一個(gè)線程的行為的響應(yīng)。如果另一個(gè)線程的行為也是對(duì)另一個(gè)線程的行為的響應(yīng),這時(shí)活性鎖可能就產(chǎn)生了。與死鎖比較,活性鎖線程是不能得到更進(jìn)一步的進(jìn)步。但是這些線程并沒有被阻塞--它們只是過分疲于應(yīng)付彼此而不能恢復(fù)工作。就能比喻成在走廊中的兩個(gè)人都試圖通過對(duì)方:Alphonse向他的左邊移動(dòng)以讓Gaston通過,此時(shí)Gaston則向他的右邊移動(dòng)以讓Alphonse通過。看到他們?nèi)匀槐舜俗枞珹lphone就向他的右邊移動(dòng),此時(shí)Gaston向他的左邊移動(dòng)。這樣,他們?nèi)匀桓髯宰枞鴮?duì)方...
受保護(hù)的塊
線程經(jīng)常不得不調(diào)整它們的行為。最常用的調(diào)整方式就是受保護(hù)的塊。在執(zhí)行之前,這種塊開始時(shí)會(huì)輪詢查檢某個(gè)條件必須為成立。為了正確地做到這一點(diǎn)有許多步驟需要遵守。
例如,假設(shè)guardedJoy方法將不會(huì)執(zhí)行,直到共享變量joy被別的線程設(shè)置過。理論上,這樣的一個(gè)方法可以不停的循環(huán)直到條件滿足為止。但這個(gè)循環(huán)不經(jīng)濟(jì),因?yàn)樵诘却臅r(shí)候它仍然在持續(xù)不停的運(yùn)行。
public void guardedJoy() {
//Simple loop guard. Wastes processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
一種更高效的保護(hù)方式就是調(diào)用Object.wait方法去暫停當(dāng)前的線程。調(diào)用wait方法不會(huì)返回直到另一個(gè)線程發(fā)出通知,說某個(gè)特定事件已經(jīng)發(fā)生過了--盡管這個(gè)線程所等待的事件并不是必要的:
public synchronized guardedJoy() {
//This guard only loops once for each special event, which may not
//be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
注意:總是在循環(huán)內(nèi)部調(diào)用wait方法去測(cè)試所等待的條件是否成立。不要猜想你所等待著的特殊條件中斷了,或者這個(gè)條件仍然成立。
就像許多延緩執(zhí)行的方法一樣,wait也會(huì)拋出InterruptedException異常。在這個(gè)例子中,我們可以忽略這個(gè)異常--我們僅關(guān)注joy的值。
為什么guardedJoy的這個(gè)版本是可同步的?假設(shè)我們用調(diào)用wait方法的對(duì)象是d,當(dāng)一個(gè)線程調(diào)用了wait方法,它必須擁有對(duì)象d的內(nèi)部鎖--否則一個(gè)錯(cuò)誤就會(huì)發(fā)生。在一個(gè)同步方法內(nèi)部調(diào)用wait方法是一種獲得內(nèi)部鎖的簡(jiǎn)便途徑。
當(dāng)wait方法被調(diào)用了,該線程就釋放鎖并掛起執(zhí)行。在以后的某個(gè)時(shí)間,另一個(gè)線程將會(huì)獲得相同的鎖并調(diào)用Object.notifyALL方法,通知所有正在等待這個(gè)鎖的線程某個(gè)重要的事情已經(jīng)發(fā)生過了:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
在第二個(gè)線程已經(jīng)釋放鎖之后的某個(gè)時(shí)間,第一個(gè)線程重新獲得鎖并從wait方法的調(diào)用中返回以恢復(fù)執(zhí)行。
注意:還有另一個(gè)通知方法,notify,該方法只喚醒一個(gè)線程。因?yàn)閚otify方法不允許你指定被喚醒的線程,所以它只用于大并發(fā)應(yīng)用程序中--即,這個(gè)程序擁有大量的線程,這些線程又都做類似的事情。在這樣的應(yīng)用中,你并不關(guān)心是哪個(gè)線程被喚醒了。
讓我們使用受保護(hù)的塊去創(chuàng)建生產(chǎn)者-消費(fèi)者應(yīng)用。該種應(yīng)用是在兩個(gè)線程中共享數(shù)據(jù):生產(chǎn)者創(chuàng)建數(shù)據(jù),而消費(fèi)者使用數(shù)據(jù)。這兩個(gè)線程使用一個(gè)共享對(duì)象進(jìn)行通信。協(xié)調(diào)是必須的:消費(fèi)者線程在生產(chǎn)者線程交付這個(gè)數(shù)據(jù)之前不能試圖去獲取它,在消費(fèi)者還沒有獲取老的數(shù)據(jù)之前生產(chǎn)者不能試圖交付新的數(shù)據(jù)。
在這個(gè)例子中,數(shù)據(jù)是一系列的文本信息,它們通過類型為Drop的對(duì)象進(jìn)行共享:
public class Drop {
//Message sent from producer to consumer.
private String message;
//True if consumer should wait for producer to send message, false
//if producer should wait for consumer to retrieve message.
private boolean empty = true;
public synchronized String take() {
//Wait until message is available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {}
}
//Toggle status.
empty = true;
//Notify producer that status has changed.
notifyAll();
return message;
}
public synchronized void put(String message) {
//Wait until message has been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {}
}
//Toggle status.
empty = false;
//Store message.
this.message = message;
//Notify consumer that status has changed.
notifyAll();
}
}
生產(chǎn)者進(jìn)程,由Producer類定義,傳遞一系列類似的信息。字符串"DONE"表示所有的信息都已經(jīng)發(fā)出了。為了模擬真實(shí)應(yīng)用的不可能預(yù)知性,生產(chǎn)者線程在兩次發(fā)送信息之間會(huì)暫停一個(gè)隨機(jī)的時(shí)間間隔。
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0; i < importantInfo.length; i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
drop.put("DONE");
}
}
消費(fèi)者線程,由Consumer類定義,就獲得信息并把它們打印出來,直到獲得"DONE"對(duì)象為止。該線程也會(huì)在隨機(jī)的時(shí)間間隔內(nèi)暫停執(zhí)行。
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take(); ! message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
}
}
最后就是main線程了,定義在了ProducerConsumerExample類中,該類將啟動(dòng)生產(chǎn)者和消費(fèi)者線程。
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}
注意:Drop類是為了證明受保護(hù)的塊而寫的。為了避免重新發(fā)明輪子,在嘗試測(cè)試你自己的數(shù)據(jù)共享對(duì)象之前可以先使用Java集合框架中的數(shù)據(jù)結(jié)構(gòu)。
不可變對(duì)象
如果一個(gè)對(duì)象的狀態(tài)在它被創(chuàng)建之后就不能修改了,這樣的對(duì)象就被認(rèn)為是不可變的。最大程度地依賴不可變對(duì)象是一個(gè)被廣泛接受的用來創(chuàng)建簡(jiǎn)潔而可靠代碼的良好策略。
不可變對(duì)象在并發(fā)應(yīng)用中特別有用。由于它們的狀態(tài)不能改變,它們就不會(huì)有線程干預(yù)的困擾,也不會(huì)被觀察到不一致的狀態(tài)。
應(yīng)用程序員經(jīng)常不使用不可變對(duì)象,因?yàn)樗麄儞?dān)心創(chuàng)建一個(gè)新的對(duì)象而不是更新已有對(duì)象的狀態(tài)所付出的代價(jià)。創(chuàng)建對(duì)象的代價(jià)經(jīng)常被高估了,而且與不可變對(duì)象相關(guān)的高效率也可抵消一些新建對(duì)象的代價(jià)。
后面的子章節(jié)將使用一個(gè)使用可變實(shí)例的類,然后再從這個(gè)類派生出一個(gè)使用不可變實(shí)例的類。通過所做的這些,它們給出了一個(gè)進(jìn)行這種轉(zhuǎn)變的通用規(guī)則,并證明了不可變對(duì)象的一些好處。
一個(gè)同步類的例子
類SynchronizedRGB定義的對(duì)象用于表示色彩。每個(gè)對(duì)象用代表主色值的三個(gè)整數(shù)去表示色彩,并用一個(gè)字符串表示這種色彩的名稱。
public class SynchronizedRGB {
//Values must be between 0 and 255.
private int red;
private int green;
private int blue;
private String name;
private void check(int red, int green, int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public SynchronizedRGB(int red, int green, int blue, String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public void set(int red, int green, int blue, String name) {
check(red, green, blue);
synchronized (this) {
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
}
public synchronized int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public synchronized String getName() {
return name;
}
public synchronized void invert() {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
name = "Inverse of " + name;
}
}
SynchronizedRGB必須小心地避免被觀察到不一致的狀態(tài)。例如,假設(shè)一個(gè)線程執(zhí)行下面的代碼:
SynchronizedRGB color = new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2
如果另一個(gè)線程在Statement 1之后而在Statement 2之前調(diào)用了color.set方法,那么myColorInt的值就不匹配myColorName表示的色彩。為了避免這種結(jié)果,這兩條語句必須綁在一起:
synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
}
這種不一致性只可能發(fā)生在不可變對(duì)象上--對(duì)于不可變版本的SynchronizedRGB就不會(huì)有這種問題。
定義不可變對(duì)象的策略
下面的規(guī)則定義了一種簡(jiǎn)單的創(chuàng)建不可變對(duì)象的策略。不是所有被標(biāo)為"immutable"的類都符合下面的這些規(guī)則。這也不是說這些類的創(chuàng)建者缺乏考慮--他們可能有好的理由去相信他們的類的實(shí)例在構(gòu)造完畢之后就不會(huì)再改變了。
1. 不要提供"setter"方法-修改字段的方法或由這些字段引用的對(duì)象。
2. 將所有的字段設(shè)置為final和private。
3. 不允許子類去重載方法。實(shí)現(xiàn)這一點(diǎn)的最簡(jiǎn)單的路徑就是將該類聲明為final。更復(fù)雜一點(diǎn)兒的方法是聲明該類的構(gòu)造器為private,并通過工廠方法創(chuàng)建實(shí)例。
4. 如果實(shí)例字段包含對(duì)可變對(duì)象的引用,就不允許這些對(duì)象被改變:
* Don't provide methods that modify the mutable objects.
* 不要提供修改這些對(duì)象的方法。
* Don't share references to the mutable objects. Never store references to external, mutable objects passed to the constructor; if necessary, create copies, and store references to the copies. Similarly, create copies of your internal mutable objects when necessary to avoid returning the originals in your methods.
* 不要共享對(duì)可變對(duì)象的引用。不要通過構(gòu)造器存儲(chǔ)對(duì)外部可變對(duì)象的引用;如果必須那么做,就創(chuàng)建一個(gè)拷貝,將引用存放到拷貝中。類似的,如果有必要避免在你的方法內(nèi)部返回原始對(duì)象,可以為你的內(nèi)部可變對(duì)象創(chuàng)建拷貝。
將該策略應(yīng)用到SynchronizedRGB中就會(huì)產(chǎn)生如下步驟:
1. 該類中有兩個(gè)setter方法。首先是set方法,無論使用何種方式改變?cè)搶?duì)象,在該類的不可變版本中都不可能再有它的位置了。其次就是invert方法,可以使用它創(chuàng)建一個(gè)新的對(duì)象而不是修改已有的對(duì)象。
2. 所有的字段都已經(jīng)是私有的了;再進(jìn)一步使它們是final的。
3. 將該類本身聲明為final。
4. 僅有一個(gè)引用其它對(duì)象的字段,而那個(gè)對(duì)象本身也是不可變的。因此,針對(duì)包含可變對(duì)象的狀態(tài)的防護(hù)手段都是不必要的了。
做完這些之后,我們就有了ImmutableRGB:
final public class ImmutableRGB {
//Values must be between 0 and 255.
final private int red;
final private int green;
final private int blue;
final private String name;
private void check(int red, int green, int blue) {
if (red < 0 || red > 255
|| green < 0 || green > 255
|| blue < 0 || blue > 255) {
throw new IllegalArgumentException();
}
}
public ImmutableRGB(int red, int green, int blue, String name) {
check(red, green, blue);
this.red = red;
this.green = green;
this.blue = blue;
this.name = name;
}
public int getRGB() {
return ((red << 16) | (green << 8) | blue);
}
public String getName() {
return name;
}
public ImmutableRGB invert() {
return new ImmutableRGB(255 - red, 255 - green, 255 - blue,
"Inverse of " + name);
}
}
高層次并發(fā)對(duì)象
到目前為止,本教程已經(jīng)關(guān)注了在一開始就是Java平臺(tái)一部分的低層次API。這些API足以應(yīng)付非常基本的工作,但對(duì)于更高級(jí)的工作,高層次組件是必需的。對(duì)于需要充分發(fā)掘當(dāng)今多多處理器和多核系統(tǒng)的大規(guī)模并發(fā)應(yīng)用就更是如此。
在本節(jié),我們將看到一些在Java平臺(tái)5.0版中引入的高級(jí)并發(fā)特性。這些特性中的大部分是在java.util.concurrent包中實(shí)現(xiàn)的。在Java集合框架中也有一些新的并發(fā)數(shù)據(jù)結(jié)構(gòu)。
* 支持鎖機(jī)制的鎖對(duì)象簡(jiǎn)化了很多并發(fā)應(yīng)用。
* 執(zhí)行器為啟動(dòng)和管理線程定義了一個(gè)高級(jí)API。由java.util.concurrent包提供的執(zhí)行器的實(shí)現(xiàn)提供了適應(yīng)于大規(guī)模應(yīng)用的線程池管理。
* 并發(fā)集合使得管理大型數(shù)據(jù)集合更為簡(jiǎn)易,并能大幅減少去同步的需求。
* 原子變量擁有最小化對(duì)同步的需求并幫助避免內(nèi)存一致性錯(cuò)誤的特性。
鎖對(duì)象
同步代碼依賴于一種簡(jiǎn)單的可重入鎖。這種鎖方便使用,但有很多限制。java.util.concurrent.locks包提供了更為復(fù)雜的鎖機(jī)制。我們不會(huì)細(xì)致地測(cè)試這個(gè)包,而是關(guān)注它最基本的接口,Lock。
鎖對(duì)象工作起來非常像由同步代碼使用的隱含鎖。使用隱含鎖時(shí),在一個(gè)時(shí)間點(diǎn)只有一條線程能夠擁有鎖對(duì)象。通過與之相關(guān)聯(lián)的Condition對(duì)象,鎖對(duì)象也支持等待/喚醒機(jī)制。
相對(duì)于隱含鎖,鎖對(duì)象最大的優(yōu)勢(shì)就是它們可以退回(backs out)試圖去獲得某個(gè)鎖。如果一個(gè)鎖不能立刻或在一個(gè)時(shí)間限制(如果指定了)之前獲得的話,tryLock方法就會(huì)退回這一企圖。如果在獲得鎖之前,另一個(gè)線程發(fā)出了中斷信號(hào),lockInterruptibly方法也會(huì)退回這一請(qǐng)求。
讓我們使用鎖對(duì)象去解決在Liveness這一節(jié)中看到的死鎖問題。Alphonse和Gaston已經(jīng)訓(xùn)練了他們自己,能夠注意到朋友對(duì)鞠躬的反應(yīng)。我們使用這樣一種方法去做改進(jìn),即要求Friend對(duì)象在應(yīng)對(duì)鞠躬行為之前,必須獲得兩個(gè)參與者的鎖。下面的源代碼就是改進(jìn)后的模型,Safelock。為了證明該方式的通用性,我們假設(shè)Alphonse和Gaston是如此地癡迷于他們新發(fā)現(xiàn)的安全地鞠躬的能力,以至于相互之間都不能停止向?qū)Ψ骄瞎?br /> import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.Random;
public class Safelock {
static class Friend {
private final String name;
private final Lock lock = new ReentrantLock();
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public boolean impendingBow(Friend bower) {
Boolean myLock = false;
Boolean yourLock = false;
try {
myLock = lock.tryLock();
yourLock = bower.lock.tryLock();
} finally {
if (! (myLock && yourLock)) {
if (myLock) {
lock.unlock();
}
if (yourLock) {
bower.lock.unlock();
}
}
}
return myLock && yourLock;
}
public void bow(Friend bower) {
if (impendingBow(bower)) {
try {
System.out.format("%s: %s has bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
} finally {
lock.unlock();
bower.lock.unlock();
}
} else {
System.out.format("%s: %s started to bow to me, but" +
" saw that I was already bowing to him.%n",
this.name, bower.getName());
}
}
public void bowBack(Friend bower) {
System.out.format("%s: %s has bowed back to me!%n",
this.name, bower.getName());
}
}
static class BowLoop implements Runnable {
private Friend bower;
private Friend bowee;
public BowLoop(Friend bower, Friend bowee) {
this.bower = bower;
this.bowee = bowee;
}
public void run() {
Random random = new Random();
for (;;) {
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {}
bowee.bow(bower);
}
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(new BowLoop(alphonse, gaston)).start();
new Thread(new BowLoop(gaston, alphonse)).start();
}
}
執(zhí)行器
在所有之前的例子中,在由新線程執(zhí)行的工作--由它的Runnable對(duì)象定義,和該線程本身--由Thread對(duì)象定義,之間有著緊密的聯(lián)系。對(duì)于小規(guī)模應(yīng)用,它能工作的很好,但在大規(guī)模應(yīng)用中,就有必要將線程的管理與創(chuàng)建從應(yīng)用的其它部分分離出來。封閉這部分功能的對(duì)象被稱作執(zhí)行器。下面的子章節(jié)將細(xì)致地描述執(zhí)行器。
* Executor接口定義了三種執(zhí)行器類型。
* Thread Pool是執(zhí)行器最常用的實(shí)現(xiàn)。
Executor接口
java.util.concurrent包定義了三種執(zhí)行器接口:
* Executor,是一種支持啟動(dòng)新任務(wù)的簡(jiǎn)單接口。
* ExecutorService,是Executor接口的子接口,它加入了幫助管理包括單個(gè)任務(wù)和執(zhí)行器本身的生命周期的功能。
* ScheduledExecutorService,是ExecutorService的子接口,支持在未來時(shí)間和/或周期性執(zhí)行任務(wù)。
一般地,與執(zhí)行器對(duì)象相關(guān)的變量都被聲明為上述三個(gè)接口中的一個(gè),而不是一個(gè)執(zhí)行器類。
Executor接口
Executor接口提供了一個(gè)方法,execute,它被設(shè)計(jì)為是通用的線程創(chuàng)建方式的替代品。如果r是一個(gè)Runnable對(duì)象,那么e就是你用于替換r的Executor對(duì)象
(new Thread(r)).start();
和
e.execute(r);
然而,execute方法的定義缺乏規(guī)范。前面的低層次機(jī)制創(chuàng)建了一個(gè)新的線程并立即啟動(dòng)它。但根據(jù)Executor的不同實(shí)現(xiàn),execute可能也會(huì)做相同的事情,但更可能是使用一個(gè)已有的工人(worker)線程去執(zhí)行r,或?qū)置于一個(gè)等待工人線程能被使用的隊(duì)列中。(我們將在Thread Pool一節(jié)中描述工人線程。)
java.util.concurrent包中的執(zhí)行器實(shí)現(xiàn)被設(shè)計(jì)為使更高級(jí)的ExecutorService和ScheduledExecutorService能夠充分使用它,盡管它們也能夠與Executor接口一起工作。
ExecutorService接口
ExecutorService接口補(bǔ)充提供了一個(gè)與execute相似但功能更豐富的submit方法。與execute相同,submit方法也接受Runnable對(duì)象,但也接受Callable對(duì)象,該對(duì)象允許任務(wù)返回一個(gè)值。submit方法返回Future對(duì)象,該對(duì)象用于獲取Callable返回的值并管理Callable和Runnable任務(wù)的狀態(tài)。
ExecutorService也提供了用于提交大量Callable對(duì)象集合的方法。最后,ExecutorService還提供了用于管理執(zhí)行器關(guān)閉的一組方法。為了支持立即關(guān)閉,任務(wù)應(yīng)該要正確地處理中斷。
ScheduledExecutorService接口
ScheduledExecutorService接口為它的父接口補(bǔ)充提供了與時(shí)間計(jì)劃有關(guān)的方法,使得能在指定延遲后執(zhí)行Runnable或Callable任務(wù)。
線程池
java.util.concurrent包中的大部分執(zhí)行器實(shí)現(xiàn)都使用了由工人(worker)線程組成的線程池。這種線程獨(dú)立于Runnable和Callable任務(wù)而存在,常被用于執(zhí)行多個(gè)任務(wù)。
使用工人線程能夠最大限度的減小由于線程創(chuàng)建而產(chǎn)生的開銷。線程對(duì)象會(huì)占用大量的內(nèi)存,而在大規(guī)模應(yīng)用中,分配和收回大量線程對(duì)象會(huì)造成大量的內(nèi)存管理開銷。
一種常用的線程池類型是固定數(shù)量線程池。這種池總是有特定數(shù)量的線程在執(zhí)行;如果一個(gè)線程不知何故在它仍然被使用時(shí)終止了,它會(huì)立即被一個(gè)新的線程替代。任務(wù)通過內(nèi)部隊(duì)列提交到線程池中。當(dāng)活躍任務(wù)的數(shù)量超過線程的數(shù)量時(shí),這種內(nèi)部隊(duì)列會(huì)保存多余的任務(wù)。
固定數(shù)量線程池的一個(gè)重要的優(yōu)點(diǎn)就是應(yīng)用會(huì)慢慢退化地使用它。為了理解這一點(diǎn),考慮這樣的一個(gè)Web服務(wù)器的應(yīng)用,每個(gè)HTTP請(qǐng)求被獨(dú)立的線程處理。如果應(yīng)用為每個(gè)新的HTTP請(qǐng)求創(chuàng)建一個(gè)新的線程,并且系統(tǒng)接到的請(qǐng)求數(shù)超出它能立即處理的數(shù)量,即當(dāng)所有線程的開銷超過系統(tǒng)的承受能力時(shí),該應(yīng)用對(duì)此的反應(yīng)就會(huì)是突然停止 。
一種簡(jiǎn)單地創(chuàng)建執(zhí)行器的方法就是使用固定數(shù)量線程池,通過調(diào)用java.util.concurrent.Executors類的newFixedThreadPool工廠方法可以得到該線程池。Executors類也提供下面的工廠方法:
* newCachedThreadPool方法創(chuàng)建一個(gè)有可擴(kuò)展線程池的執(zhí)行器。該執(zhí)行器適用于會(huì)啟動(dòng)許多短壽命任務(wù)的應(yīng)用。
* newSingleThreadExecutor方法創(chuàng)建在一個(gè)時(shí)間點(diǎn)只執(zhí)行一個(gè)任務(wù)的執(zhí)行器。
* 有幾個(gè)工廠方法是上述執(zhí)行器的ScheduledExecutorService版本。
如果上述工廠方法提供的執(zhí)行器沒有一個(gè)適合于你的需求,創(chuàng)建java.util.concurrent.ThreadPoolExecutor或java.util.concurrent.ScheduledThreadPoolExecutor的實(shí)例將給你另外的選擇。
并發(fā)集合
java.util.concurrent包含一組Java集合框架額外的擴(kuò)展。根據(jù)提供的集合接口十分容易把它們歸類為:
* BlockingQueue定義了一個(gè)先入先出的數(shù)據(jù)結(jié)構(gòu),當(dāng)你試圖向一個(gè)已滿的隊(duì)列添加或向從一個(gè)已空的隊(duì)列中取出元素時(shí),阻塞你或使你超時(shí)。
* ConcurrentMap是java.util.Map的子接口,它定義了一些有用的原子操作。只有某個(gè)鍵存在時(shí),這些操作才刪除或替換一個(gè)這個(gè)鍵-值對(duì),或者只有當(dāng)某個(gè)鍵不存在時(shí),才能添加這個(gè)鍵-值對(duì)。使這些操作都是原子的,以幫助避免同步。標(biāo)準(zhǔn)而通用的ConcurrentMap實(shí)現(xiàn)是ConcurrentHashMap,它是HashMap的同步相似體。
* ConcurrentNavigableMap is a subinterface of ConcurrentMap that supports approximate matches. The standard general-purpose implementation of ConcurrentNavigableMap is ConcurrentSkipListMap, which is a concurrent analog of TreeMap.
* ConcurrentNavigableMap是ConcurrentMap的子接口,它支持近似符合。標(biāo)準(zhǔn)而通用的ConcurrentNavigableMap是ConcurrentSkipListMap,它是TreeMap的同步相似體。
所有這些集合都為了幫助避免內(nèi)存一致性錯(cuò)誤而在向集合中添加對(duì)象的操作與其后的訪問或刪除對(duì)象的操作之間定義了"Happens-Before"關(guān)系。
原子變量
java.util.concurrent.atomic包定義了在單個(gè)變量中支持原子操作的類。所有的這些類都有g(shù)et和set方法,這些方法就如同讀寫volatile變量那樣工作。即,一個(gè)set方法與任何隨其后的針對(duì)相同變量的get方法之間有"Happen-Before"對(duì)象。通過應(yīng)用于整型原子變量的原子算術(shù)方法,原子的compareAndSet方法也戰(zhàn)士具有這樣的內(nèi)存一致性特性。
為了看看如何使用這個(gè)包,讓我們回想之前為了證明干預(yù)而使用過的類Counter:
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
為了防止線程干預(yù)的一種方法就是使它的方法可同步,如SynchronizedCounter里的方法那樣:
class SynchronizedCounter {
private int c = 0;
public synchronized void increment() {
c++;
}
public synchronized void decrement() {
c--;
}
public synchronized int value() {
return c;
}
}
對(duì)于這個(gè)簡(jiǎn)單的類,同步是一個(gè)能夠被接受的解決方案。但對(duì)于更復(fù)雜的類,我們可能想避免不必要的同步的活躍度影響。使用AtomicInteger對(duì)象替代int字段允許我們?cè)诓磺笾降那闆r下就能防止線程干預(yù)。
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger c = new AtomicInteger(0);
public void increment() {
c.incrementAndGet();
}
public void decrement() {
c.decrementAndGet();
}
public int value() {
return c.get();
}
}
進(jìn)一步地閱讀
* Concurrent Programming in Java: Design Principles and Pattern (2nd Edition),Doug Lea著。這本綜合性著作的作者是一位卓越的專家,同時(shí)也是Java平臺(tái)并發(fā)框架的架構(gòu)師。
* Java Concurrency in Practice,Brian Goetz,Tim Peierls,Joshua Bloch,Joseph Bowbeer,David Holmes和Doug Lea著。一本為貼近初學(xué)者而設(shè)計(jì)的實(shí)踐性指南。
* Effective Java Programming Language Guide,Joshua Bloch著。雖然這是一本通用的程序設(shè)計(jì)指南,但其中關(guān)于線程的章節(jié)包含著并發(fā)編程必需的"最佳實(shí)踐"。
* Concurrency: State Models & Java Programs (2nd Edition),Jeff Magee和eff Kramer著。通過模型化和實(shí)用的例子介紹了并發(fā)編程。
posted on 2007-10-28 19:51 John Jiang 閱讀(2728) 評(píng)論(5) 編輯 收藏 所屬分類: JavaSE 、Java 、Concurrency 、翻譯 、JavaTutorials