- 原文鏈接
- 譯者:靖靖
并發(fā)
進(jìn)程和線程
在并發(fā)編程當(dāng)中,有兩個(gè)基本的執(zhí)行單元:進(jìn)程和線程。在java中,我們大部分關(guān)心的是線程。然而進(jìn)程也很重要。
一個(gè)電腦系統(tǒng)通常有許多活躍的進(jìn)程和線程。在只有一個(gè)核心的系統(tǒng)當(dāng)中,在任意一個(gè)時(shí)刻,實(shí)際上只有一個(gè)線程在執(zhí)行。進(jìn)程和線程通過操作系統(tǒng)的時(shí)間分片特性共享單個(gè)核心的處理時(shí)間。
進(jìn)程
一個(gè)進(jìn)程有獨(dú)立的執(zhí)行環(huán)境。進(jìn)程一般有完整的、私有的基本運(yùn)行資源。尤其要說的每個(gè)進(jìn)程都有自己的獨(dú)立內(nèi)存空間。
進(jìn)程經(jīng)常被視作一個(gè)程序或者是一個(gè)應(yīng)用。然而那些被用戶視作的單個(gè)應(yīng)用程序進(jìn)程也許實(shí)際上由一些相互協(xié)作的進(jìn)程組合而成。為了促進(jìn)進(jìn)程間的通信,大部分操作系統(tǒng)支持內(nèi)部進(jìn)程交流(Inter Process Communication[IPC])資源,例如管道和套接字。IPC不僅僅可以在同一個(gè)系統(tǒng)的進(jìn)程間進(jìn)行通信,也可以在不同的系統(tǒng)的進(jìn)程間進(jìn)行通信。
大多數(shù)的Java虛擬機(jī)的實(shí)現(xiàn)都是以單個(gè)進(jìn)程的方式運(yùn)行的。一個(gè)Java應(yīng)用程序可以用ProcessBuilder對(duì)象創(chuàng)建額外的進(jìn)程。多進(jìn)程應(yīng)用程序不再這節(jié)課的討論范圍之內(nèi)。
線程
線程有的時(shí)候叫做輕量級(jí)的進(jìn)程。進(jìn)程和線程都提供一個(gè)執(zhí)行的環(huán)境,但是創(chuàng)建一個(gè)新的線程所需要的資源比創(chuàng)建一個(gè)進(jìn)程所需要的資源少。
線程存在在進(jìn)程之中,每一個(gè)進(jìn)程至少有一個(gè)線程。線程共享進(jìn)程的資源,包括內(nèi)存和打開的文件。這樣設(shè)計(jì)是為了提高效率和交流,但是可能帶來隱藏的問題。
多線程執(zhí)行是Java虛擬機(jī)的基本特性。如果你計(jì)數(shù)系統(tǒng)的線程像內(nèi)存管理和信號(hào)處理一樣,那么每一個(gè)應(yīng)用程序至少有一個(gè)線程或者是多個(gè)。但是從應(yīng)用程序開發(fā)者的角度看,你僅僅只調(diào)用了一個(gè)叫做main thread的線程。這個(gè)線程有能力去創(chuàng)建額外的線程,我們將在下章節(jié)進(jìn)行講解。
線程對(duì)象
每一個(gè)線程都可以和類Thread的一個(gè)實(shí)例聯(lián)系起來。有兩種基本的策略使用線程對(duì)象創(chuàng)建一個(gè)并發(fā)的應(yīng)用程序。
- 為了直接控制線程的創(chuàng)建和管理,僅僅只在應(yīng)用程序需要啟動(dòng)一個(gè)異步任務(wù)的時(shí)候?qū)嵗€程
- 為了抽象線程的管理,可以將應(yīng)用程序的任務(wù)加入到executor中。
本節(jié)介紹Thread對(duì)象的使用。Executors將和更高級(jí)別并行對(duì)象一起討論。
線程的定義和啟動(dòng)
一個(gè)程序創(chuàng)建一個(gè)線程的實(shí)例必須提供需要運(yùn)行的代碼。下面有兩種實(shí)現(xiàn)的方法:
-
提供一個(gè)Runnable的對(duì)象。Runnable接口定義了一個(gè)方法run,這個(gè)方法里面包含了在線程當(dāng)中執(zhí)行的代碼。Runnable對(duì)象可以做為Thread的構(gòu)造器參數(shù)就像下面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接口,Thread的run方法什么都沒有做。程序可以繼承類Thread,實(shí)現(xiàn)自己的run方法就像下面的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(); } } `
請(qǐng)注意兩個(gè)例子都是調(diào)用Thread的start的方法啟動(dòng)新的線程。
你應(yīng)該使用哪一種方法?第一種方法實(shí)現(xiàn)Runnable接口,這種方法使用更加普遍,因?yàn)槌薚hread類,這個(gè)對(duì)象還可以繼承其他類。第二種方法在簡(jiǎn)單的程序中使用起來更簡(jiǎn)單,但是有個(gè)限制就是你的任務(wù)類必須是Thread的子類。這一節(jié)主要是聚焦在第一種方法上,它把用Runnable任務(wù)和用Thread對(duì)象去執(zhí)行任務(wù)區(qū)分開來。這種方法不僅僅更加靈活,而且適用性更高,這點(diǎn)在后面更高級(jí)別的線程管理APIs中會(huì)提到。
類Thread定義了一些有用的線程管理的方法。包括提供一些關(guān)于調(diào)用方法的線程的信息和影響線程狀態(tài)的靜態(tài)方法。被其他線程調(diào)用的一些方法也可以管理線程和Thread對(duì)象。我們將在接下來的章節(jié)中學(xué)習(xí)它們中的一些方法。
用Sleep方法暫停線程的執(zhí)行
Thread的sleep方法調(diào)用會(huì)讓當(dāng)前執(zhí)行的線程暫停一段特定的時(shí)間。這是讓運(yùn)行在電腦系統(tǒng)上的應(yīng)用或者其他應(yīng)用的其他線程可以占用處理器時(shí)間的有效方式。Sleep方法也可以讓線程一步一步的執(zhí)行,就像下面例子中展示的,或者是等待另外一個(gè)有時(shí)間需求的線程,就像后面章節(jié)中介紹的SimpleThreads例子。
提供了兩個(gè)重載的sleep方法:也是休眠時(shí)間單位是微秒,另外一個(gè)休眠的時(shí)間單位是納米。然而,這些休眠的時(shí)間不能保證是準(zhǔn)確的,因?yàn)樗鼈兪芟抻诓僮飨到y(tǒng)之下的硬件設(shè)備。同時(shí),休眠過程中也可以被中斷終止,我們將在后面的章節(jié)看到。在任何情況下,你不可以認(rèn)為調(diào)用sleep方法可以讓線程暫停指定的準(zhǔn)確的時(shí)間。
SleepMessages例子用sleep方法實(shí)現(xiàn)每4秒打印消息:
`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函數(shù)聲明拋出InterruptedException。當(dāng)一個(gè)線程在睡眠當(dāng)中,另外一個(gè)線程中斷這個(gè)線程,那么sleep方法將拋出InterruptedException。因?yàn)檫@個(gè)程序沒有定義另外一個(gè)線程去調(diào)用中斷,所以就沒有寫catch語句去捕獲InterruptedException。
中斷
一個(gè)中斷是告訴一個(gè)線程它應(yīng)該暫停它正在做的事情。線程怎么去回應(yīng)這個(gè)中斷完全取決于開發(fā)者的決定,但是讓線程終止也是很正常的決定。這是在這一節(jié)著重介紹的用處。一個(gè)線程通過調(diào)用Thread對(duì)象的interrupt方法發(fā)送一個(gè)中斷給到需要中斷的線程。為了讓中斷機(jī)制運(yùn)行正確,被中斷的線程必須支持自己的中斷。
支持中斷
一個(gè)線程怎樣支持它自己的中斷列?這個(gè)依賴于它正在做什么。假如說一個(gè)線程經(jīng)常調(diào)用拋出InterruptedException的方法,在它捕獲這個(gè)異常之后它僅僅是從run方法中返回。例如,假如在SleepMessages例子中的主要循環(huán)語句在Runnable對(duì)象的run方法當(dāng)中。然后它可以被修改成下面的形式支持中斷:
`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]);
}`
很多方法拋出InterruptedException,例如sleep方法,這些方法被設(shè)計(jì)成當(dāng)它們收到中斷時(shí)立馬取消當(dāng)前的操作和立即返回。
假如有個(gè)線程沒有調(diào)用拋出InterruptedException的方法,那它怎樣去響應(yīng)中斷列?那么它必須定期的去調(diào)用Thread的interrupted方法,這個(gè)方法在該線程設(shè)置了中斷的情況下返回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)單的例子當(dāng)中,代碼僅僅檢查中斷如果那個(gè)線程已經(jīng)接收了中斷,那么這個(gè)check返回true。在很多復(fù)雜的應(yīng)用當(dāng)中,拋出一個(gè)InterruptedException更加讓人明白:
`if (Thread.interrupted()) {
throw new InterruptedException();
}`
這個(gè)例子讓中斷處理的代碼集中到catch的語句當(dāng)中。
中斷狀態(tài)標(biāo)志
中斷原理是用內(nèi)部的一個(gè)叫做中斷狀態(tài)的標(biāo)志來實(shí)現(xiàn)的。調(diào)用Thread的interrupt方法會(huì)設(shè)置這個(gè)標(biāo)志。當(dāng)一個(gè)線程通過調(diào)用Thread的靜態(tài)方法interrupted檢查中斷時(shí),中斷狀態(tài)會(huì)被清除。一個(gè)線程會(huì)用另外一個(gè)線程的非靜態(tài)的isInterrupted方法來查詢它的中斷狀態(tài),這個(gè)操作不會(huì)改變另外一個(gè)線程的中斷狀態(tài)標(biāo)志。
按照規(guī)定,任何一個(gè)可以拋出InterruptedException的方法在拋出InterruptedException之后會(huì)清除中斷狀態(tài)。然而,也很有可能,這個(gè)中斷狀態(tài)會(huì)立馬被另外一個(gè)線程調(diào)用interrupt方法設(shè)置。
Join方法
Join方法讓一個(gè)線程可以等待另外一個(gè)線程執(zhí)行完。如果t是一個(gè)正在執(zhí)行的線程對(duì)象,
`t.join();`
這個(gè)調(diào)用會(huì)讓當(dāng)前線程停止執(zhí)行直到t執(zhí)行完成。join的方法的重載讓開發(fā)者可以指定特定的等待時(shí)間。然而和sleep方法一樣,join方法等待的時(shí)間依賴于操作系統(tǒng),因此你不可以認(rèn)為join方法會(huì)準(zhǔn)確的等待你所指定的時(shí)間。
就像sleep方法,join通過拋出InterruptedException來響應(yīng)中斷。
SimpleThreads例子
下面的這個(gè)例子把這部分的一些概念綜合起來進(jìn)行展示。SimpleThreads包含兩個(gè)線程。第一個(gè)線程是每個(gè)Java程序都會(huì)有的主線程。主線程通過Runnable對(duì)象創(chuàng)建了一個(gè)叫MessageLoop的新線程,然后主線程等待這個(gè)線程完成。如果MessageLoop這個(gè)線程花費(fèi)了太久都沒有完成,那么主線程就會(huì)中斷這個(gè)線程。
MessageLoop線程會(huì)打印一系列的消息。如果在打印完所有消息之前中斷這個(gè)線程,MessageLoop線程將打印一個(gè)消息然后退出。
`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ì)屬性和對(duì)象引用的屬性的共享訪問實(shí)現(xiàn)的。這種形式的通信效率特別高,但是會(huì)帶來兩種可能的錯(cuò)誤:線程干擾和內(nèi)存一致性錯(cuò)誤。防止這類錯(cuò)誤發(fā)生的工具就是同步。
然而,同步又會(huì)引入線程競(jìng)爭(zhēng)問題,這種問題在兩個(gè)或者多個(gè)線程同時(shí)去訪問相同的資源的時(shí)候會(huì)發(fā)生,會(huì)讓Java執(zhí)行一些線程變的更慢甚至可能暫停它們的執(zhí)行。饑餓和活鎖是線程競(jìng)爭(zhēng)的表現(xiàn)形式??梢栽谡鹿?jié)活鎖中了解關(guān)于這方面更多信息。
這一節(jié)主要是講解下面這些話題:
- 線程干擾是描述多線程訪問共享數(shù)據(jù)時(shí)錯(cuò)誤是怎么引入的。
- 內(nèi)存一致性錯(cuò)誤描述的是共享內(nèi)存的不一致性錯(cuò)誤。
- 同步方法描述的是一種有效的防止線程干擾和內(nèi)存一致性錯(cuò)誤的方法。
- 隱式鎖和同步描述的是一種更加普遍的基于隱士鎖的同步方法。
- 原子性討論的是不能被其他線程干擾的操作的大體概念。
線程干擾
下面有個(gè)叫做Counter的簡(jiǎn)單類
`class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}`
Counter類的每次increment的方法的調(diào)用會(huì)讓c的值加一,decrement的方法的調(diào)用會(huì)讓c的值減一。然而,如果這個(gè)Counter對(duì)象被多線程引用,線程之間的干擾讓它沒有按照預(yù)期的運(yùn)行。
當(dāng)兩個(gè)操作在不同的線程間在同樣的數(shù)據(jù)上交替運(yùn)行時(shí)會(huì)產(chǎn)生線程干擾。也就是說這兩個(gè)操作包含多個(gè)步驟,然后步驟的序列產(chǎn)生了重疊。
看起來似乎在Counter的實(shí)例對(duì)象上面的操作交替進(jìn)行是不可能的,因?yàn)閮蓚€(gè)在變量c上面的操作都是單個(gè)的、簡(jiǎn)單的語句。然而,盡管簡(jiǎn)單的語句也可以被虛擬機(jī)轉(zhuǎn)化成為多個(gè)步驟執(zhí)行。我們不需要檢查到底虛擬機(jī)會(huì)花了多少步,只需要知道表達(dá)式c++被分解成3部就足夠了:
- 取得c現(xiàn)在的值。
- 在取得的值上面增加1.
- 在把增加之后的值存儲(chǔ)到c中。
表達(dá)式c--也會(huì)被按照相同的方式分解,除了把第二步的增加換為減少就行了。
假定線程A調(diào)用了increment方法,在同一時(shí)刻,線程B調(diào)用了decrement方法。如果c的初始值是0,它們交替執(zhí)行的序列可能是下面這樣:
- 線程A:取得c的值。
- 線程B:取得c的值。
- 線程A:增加取得的值;結(jié)果是1。
- 線程B:減少取得的值;結(jié)果是-1。
- 線程A:存儲(chǔ)結(jié)果到c中;c的值現(xiàn)在是1。
- 線程B:存儲(chǔ)結(jié)果到c中;c的值現(xiàn)在是-1。
線程A執(zhí)行的結(jié)果被線程B重寫了。這種交替序列僅僅是其中的一種可能。在不同的情況下,可能是線程B的結(jié)果丟失,或者是得到預(yù)期的結(jié)果。因?yàn)榫€程干擾的bugs是不可預(yù)測(cè)的。
內(nèi)存一致性錯(cuò)誤
數(shù)據(jù)一致性錯(cuò)誤發(fā)生在不同的線程去讀相同的數(shù)據(jù)時(shí),讀到的數(shù)據(jù)不一致。引起內(nèi)存一致性錯(cuò)誤的原因很復(fù)雜超出了這篇教程的范圍。幸運(yùn)的是,開發(fā)者不需要詳細(xì)知道這些原因。開發(fā)者需要知道的是如何去避免這些錯(cuò)誤。
避免內(nèi)存一致性錯(cuò)誤的關(guān)鍵是理解happens-before關(guān)系。這個(gè)關(guān)系僅僅是保證一塊內(nèi)存被一個(gè)特定的語句的寫操作對(duì)另外一個(gè)特定的語句是可見的。為了理解上面的這句話,我們來看下下面的例子。假定定義了一個(gè)簡(jiǎn)單的int類型的屬性,且初始值是0:
`int counter = 0;`
這個(gè)屬性在線程A和B之間是共享的。假定線程A增加了counter的值:
`counter++;`
然后在很短的時(shí)間內(nèi),線程B打印了counter的值:
`System.out.println(counter);`
如果這個(gè)兩個(gè)語句在同一個(gè)線程當(dāng)中執(zhí)行,那么這個(gè)屬性被打印出來的值肯定是‘’1‘’。但是如果兩個(gè)語句在不同的線程當(dāng)中執(zhí)行,那么打印出來的值可能就是“0”,因?yàn)闆]有保證線程A對(duì)屬性counter的改變對(duì)線程B是可見的,除非開發(fā)者在這兩條語句之間建立了happens-before的關(guān)系。
有數(shù)種建立happens-before關(guān)系的行為。其中一種就是同步,這個(gè)我們將在接下來的章節(jié)當(dāng)中看到。
我們已經(jīng)看到下面兩種行為會(huì)創(chuàng)建happens-before關(guān)系:
- 當(dāng)一個(gè)語句調(diào)用Thread.start方法時(shí),那么每一個(gè)對(duì)這條語句有happens-before關(guān)系的語句對(duì)這個(gè)新線程執(zhí)行的語句都有happens-before關(guān)系。
- 當(dāng)一個(gè)線程終止執(zhí)行且在另外一個(gè)線程中調(diào)用Thread.join返回時(shí),然后所有的在這個(gè)終止的線程中執(zhí)行的語句都對(duì)那個(gè)被join的線程的接下來執(zhí)行的語句有happens-before關(guān)系。在這個(gè)線程中代碼的變化對(duì)被join的線程就是可見的。
所有的創(chuàng)建happens-before關(guān)系的行為.
同步方法
Java編程語言提供了兩種編程方式:同步方法和同步代碼塊。其中復(fù)雜的同步代碼塊在下個(gè)章節(jié)進(jìn)行講解。這個(gè)章節(jié)講解同步方法。
僅僅只需要在方法聲明前面加上關(guān)鍵字synchronized,就也可以把這個(gè)方法變成同步的:
`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è)影響:
- 首先,交替調(diào)用同一個(gè)對(duì)象的同步方法是不可能的。當(dāng)一個(gè)線程在執(zhí)行一個(gè)對(duì)象的同步方法時(shí),所有其他再調(diào)用這個(gè)對(duì)象的同步方法的線程都將被阻塞直到第一個(gè)調(diào)用的線程執(zhí)行完成。
- 其次,當(dāng)一個(gè)同步方法退出時(shí),它就自動(dòng)對(duì)后面的同一個(gè)對(duì)象的同步方法的調(diào)用建立了happens-before關(guān)系。這樣子保證了對(duì)象的狀態(tài)的改變對(duì)所有的線程都是可見的。
記住構(gòu)造器方法是不可以同步的,在構(gòu)造器方法前面加關(guān)鍵字synchronized是有語法錯(cuò)誤的。同步構(gòu)造器方法沒有任何意義,因?yàn)橹挥袆?chuàng)建這個(gè)對(duì)象的線程在這個(gè)創(chuàng)建這個(gè)對(duì)象的時(shí)候才有訪問它的權(quán)限。
*警告:當(dāng)創(chuàng)建一個(gè)在多個(gè)線程中共享的對(duì)象時(shí),要特別小心對(duì)象的引用提早泄漏出去。舉個(gè)例子,假定你想讓一個(gè)叫instances的List包含每一個(gè)類的實(shí)例。你也許會(huì)在你的構(gòu)造器中加入下面的代碼,但是然后其他線程可以在對(duì)象的構(gòu)造完成之前用instances去訪問對(duì)象(has issue,need optimize):
`instances.add(this);`
同步方法是一個(gè)簡(jiǎn)單的防止線程干擾和內(nèi)存一致性錯(cuò)誤的策略;如果一個(gè)對(duì)象對(duì)一個(gè)以上的線程是可見的,那么這個(gè)對(duì)象的變量的所有的讀和寫操作是通過同步方法完成的。(一個(gè)重要的特例:當(dāng)對(duì)象創(chuàng)建之后不可以被修改的final屬性是可以被非同步的方法安全的讀的。)這個(gè)策略很有效,但是可能會(huì)產(chǎn)生并發(fā)活躍性的問題,我們將在后面的章節(jié)看到。
內(nèi)部鎖和同步
同步時(shí)建立在一個(gè)內(nèi)部實(shí)體周圍也就是大家知道的內(nèi)部鎖或者叫監(jiān)控鎖。(API文檔里面幾次把這實(shí)體叫做監(jiān)控)內(nèi)部鎖不僅僅在同步方面(強(qiáng)制排它的訪問對(duì)象的狀態(tài))起到作用,同時(shí)也在建立happens-before關(guān)系(對(duì)可見性是必須的)起到作用。
每一個(gè)對(duì)象都有一個(gè)和它相關(guān)聯(lián)的內(nèi)部鎖。一個(gè)需要排它和一致訪問對(duì)象屬性的線程在訪問對(duì)象屬性之前必須要先獲得對(duì)象的內(nèi)部鎖,然后完成之后釋放內(nèi)部鎖。也就是線程在持有這個(gè)對(duì)象的內(nèi)部鎖在獲得和釋放這個(gè)鎖之間。只要一個(gè)線程持有了一個(gè)內(nèi)部鎖,那么其他的線程就都不可以拿到這個(gè)內(nèi)部鎖。其他的線程會(huì)被阻塞當(dāng)它們嘗試去獲得這個(gè)鎖時(shí)。
當(dāng)一個(gè)線程釋放一個(gè)內(nèi)部鎖時(shí),在這個(gè)動(dòng)作和任意后面的獲得這個(gè)鎖的動(dòng)作建立了happens-before關(guān)系。
在同步方法中的鎖
當(dāng)一個(gè)線程調(diào)用一個(gè)同步方法時(shí),它會(huì)自動(dòng)去獲得這個(gè)方法的對(duì)象的內(nèi)部鎖,然后當(dāng)這個(gè)方法退出時(shí)釋放這個(gè)鎖,即使是這個(gè)方法的退出是因?yàn)闆]有捕獲的異常引起的。
你也許想知道當(dāng)一個(gè)同步的靜態(tài)方法被調(diào)用時(shí)會(huì)發(fā)生什么,因?yàn)橐粋€(gè)靜態(tài)的方法是和類相關(guān)聯(lián)的,不是對(duì)象。在這種情況下,線程獲得的是和這個(gè)類相關(guān)的對(duì)象的內(nèi)部鎖。因此控制訪問類的靜態(tài)屬性的鎖是和任何類的實(shí)例的鎖是不一樣的。
同步代碼塊
另外一個(gè)種建立同步代碼的的方法是同步代碼塊。和同步方法不一樣,同步代碼塊必須指定哪一個(gè)對(duì)象的內(nèi)部鎖:
`public void addName(String name) {
synchronized(this) {
lastName = name;
nameCount++;
}
nameList.add(name);
}`
在這個(gè)例子中,addName方法需要同步的改變lastName和nameCount的屬性的值,但是同時(shí)又需要避免其他對(duì)象方法的同步調(diào)用。(通過同步代碼調(diào)用其他對(duì)象的方法會(huì)帶來像章節(jié)Liveness。)如果沒有同步代碼,則必須要有單獨(dú)的非同步的方法去調(diào)用nameList.add。
同步代碼塊用細(xì)粒度的鎖可以提高并發(fā)。舉個(gè)例子,類MsLunch有兩個(gè)實(shí)例屬性,c1和c2,它們從來不回被同時(shí)使用。所有的這兩個(gè)屬性的更新必須是同步的,如果c1的更新和c2 的更新用的是同一個(gè)對(duì)象的鎖,那么在c1和c2交替更新時(shí)會(huì)造成不必要的阻塞從而降低了并發(fā)。我們可以創(chuàng)建兩個(gè)對(duì)象單獨(dú)的提供鎖來代替使用同步方法獲使用this鎖。
`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)慎的方式。你一定可以完全確認(rèn)交替的去訪問受影響的屬性是線程安全的。
重入同步
回想一下,一個(gè)線程是不可以獲得一個(gè)被其他線程持有的鎖。但是一個(gè)線程可以獲得他自己持有的鎖。允許一個(gè)線程可以獲得多次獲得同一個(gè)鎖讓重入同步成為可能。這個(gè)描述的是這樣一個(gè)場(chǎng)景,一個(gè)同步的代碼直接或者間接的調(diào)用一個(gè)包含同步的代碼的方法,且這兩個(gè)同步包含的同一個(gè)鎖。如果沒有重入同步特性,同步代碼不得不做很多額外的措施去避免自己把自己阻塞。
原子訪問
在編寫程序的過程中,一個(gè)原子是操作是指一次執(zhí)行完所有的操作。一個(gè)原子操作不可以在中間停下來:要么全部執(zhí)行完成,要么都沒有執(zhí)行。原子操作帶來的影響只有當(dāng)它全部完成了以后才是可見的。
我們已經(jīng)看過一個(gè)這樣的自增表達(dá)式,例如c++,這不是一個(gè)原子操作。即使是非常簡(jiǎn)單的表達(dá)式也可以定義復(fù)雜的操作,這個(gè)操作可以被分解成其它的不同的操作。然而你可以定義一組操作是原子的。
對(duì)于引用變量和大多數(shù)的簡(jiǎn)單類型的變量(除了long和double類型)的讀和寫操作都是原子類型的。
對(duì)于所有聲明為volatile的變量(包括long和double類型)的讀和寫操作都是原子類型的。
原子操作不可以被打斷,所以使用原子操作不會(huì)受線程干擾的影響。然而,這并不排除所有需要同步的原子操作的錯(cuò)誤,因?yàn)閮?nèi)存一致性錯(cuò)誤還是存在。使用volatile變量降低了內(nèi)存一致性錯(cuò)誤出現(xiàn)的風(fēng)險(xiǎn),因?yàn)槿魏螌?duì)volatile變量的寫操作都對(duì)后續(xù)的這個(gè)變量的讀操作建立了happens-before的關(guān)系。也就是說volatile變量的變化對(duì)其他的線程總是可見的。更重要的是,當(dāng)一個(gè)線程讀一個(gè)volatile的變量時(shí),不僅僅可以看到最新的變化,連代碼的副作用導(dǎo)致的變化也可以看到。
使用簡(jiǎn)單的原子變量訪問比用同步代碼去控制變量訪問更高效,但是開發(fā)者需要考慮的更多去避免內(nèi)存一致性錯(cuò)誤。這么做是否值得取決于應(yīng)用的大小和復(fù)雜性。
在java.util.concurrent的包中的一些類提供了原子的方法,這些方法不依賴同步。我們將在后續(xù)章節(jié)中討論。
活躍性
一個(gè)并發(fā)應(yīng)用及時(shí)執(zhí)行任務(wù)的能力叫做活躍性。這個(gè)章節(jié)主要介紹最常見的活躍性問題死鎖,然后會(huì)簡(jiǎn)短的介紹兩種其他的活躍性問題,饑餓和活鎖。
死鎖
死鎖描述的是這樣一個(gè)場(chǎng)景,二個(gè)或者多個(gè)線程因?yàn)榛ハ嗟却谰米枞?。舉個(gè)例子。
A和B是朋友,同時(shí)也是非常有禮貌的信徒。一個(gè)嚴(yán)格的有禮貌的規(guī)則是當(dāng)你向你的朋友鞠躬時(shí),你必須保持鞠躬的動(dòng)作直到你的朋友有機(jī)會(huì)向你鞠躬回禮。不幸的是,這個(gè)規(guī)則沒有考慮兩個(gè)朋友同時(shí)向?qū)Ψ骄瞎那闆r。下面的這個(gè)死鎖例子模擬了這種情況:
`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)這個(gè)死鎖例子運(yùn)行時(shí),兩個(gè)線程極有可能阻塞當(dāng)它們嘗試去調(diào)用bowBack方法時(shí)。兩個(gè)線程都會(huì)一直阻塞下去,因?yàn)樗鼈兌荚诘却龑?duì)方鞠躬回禮。
饑餓和活鎖
饑餓和活鎖沒有死鎖那么常見,但也是每一個(gè)并發(fā)程序設(shè)計(jì)者可能會(huì)遇到的問題。
饑餓
饑餓描述的是一種線程不能夠獲得定期訪問共享資源的場(chǎng)景,且不能夠取得進(jìn)展。饑餓發(fā)生在共享資源被“貪婪的”線程長(zhǎng)期占用造成共享資源不可用的情況下。例如,假如一個(gè)對(duì)象提供一個(gè)同步方法,然后這個(gè)同步方法經(jīng)常需要很長(zhǎng)的時(shí)間才能返回。如果一個(gè)線程經(jīng)常調(diào)用這個(gè)方法,其他需要經(jīng)常同步訪問這個(gè)對(duì)象的線程就會(huì)經(jīng)常被阻塞。
活鎖
一個(gè)線程經(jīng)常是去響應(yīng)另外一個(gè)線程的操作。如果這個(gè)另外的一個(gè)線程的執(zhí)行又是響應(yīng)另外的一個(gè)線程,然后活鎖可能就產(chǎn)生了。和死鎖一樣,活鎖線程也不能夠取得進(jìn)一步的進(jìn)展。然而這些線程并沒有被阻塞——它們僅僅是因?yàn)樘Χ荒苋セハ囗憫?yīng)然后繼續(xù)工作。這就和兩個(gè)人在一條走廊里面嘗試去超過對(duì)方:A移到他的左邊讓B通過,同事B也同時(shí)移動(dòng)他的右邊讓A通過。他們?nèi)匀蛔枞麑?duì)方。
警戒塊
線程經(jīng)常需要去調(diào)整它們的行為。最常見的調(diào)整方式是警戒塊。這個(gè)塊在可以繼續(xù)執(zhí)行之前通過輪詢一個(gè)必須為true的條件開始。為了保證這個(gè)做正確有一些步驟需要做。
假定,例如guardedJoy是一個(gè)在共享變量joy被另外一個(gè)線程設(shè)置了以后才會(huì)繼續(xù)執(zhí)行的一個(gè)方法。這個(gè)方法理論上可以一直循環(huán)直到條件滿足,但是輪詢是很浪費(fèi)的,因?yàn)樗诘却倪^程中一直在執(zhí)行。
`public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}`
一個(gè)更有效的警戒方式是調(diào)用Object的wait方法掛起當(dāng)前線程。wait方法的調(diào)用直到另外一個(gè)線程發(fā)布通知之后,才會(huì)返回,這個(gè)通知可能是一些事件已經(jīng)發(fā)生,盡管這個(gè)事件可能不是這個(gè)線程等待的:
`public synchronized void 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!");
}`
注意:你需要在檢測(cè)特定的條件的循環(huán)中調(diào)用wait方法。不要認(rèn)為這個(gè)中斷是你等待的特定的條件或者這個(gè)條件這個(gè)條件仍然是正確的。
像許多其他掛起執(zhí)行的方法一樣,wait方法也會(huì)拋出InterruptedException。在上面的這個(gè)例子中,我們可以忽略這個(gè)異常,變量joy的值是我們唯一關(guān)心的。
為什么這個(gè)版本的guardedJoy方法需要用關(guān)鍵字synchronized修飾咧?假定d是我們調(diào)用wait方法的對(duì)象。當(dāng)一個(gè)線程調(diào)用 d的wait方法時(shí),它必須要先獲得d的對(duì)象內(nèi)部鎖否則會(huì)拋出異常——調(diào)用wait方法前,必須要先獲得對(duì)象的內(nèi)部鎖。在同步方法內(nèi)部調(diào)用wait時(shí)一種簡(jiǎn)單的獲得內(nèi)部鎖的方法。
當(dāng)wait方法被調(diào)用時(shí),線程釋放了內(nèi)部鎖,掛起了執(zhí)行。在將來的某個(gè)時(shí)間,另外一個(gè)線程將獲得同一個(gè)內(nèi)部鎖,調(diào)用該對(duì)象的notifyAll方法,告訴所有等待在這個(gè)鎖上面的線程重要的事情放生了:
`public synchronized notifyJoy() {
joy = true;
notifyAll();
}`?
在第二個(gè)線程已經(jīng)釋放鎖之后的某個(gè)時(shí)間,第一個(gè)線程重新獲得了鎖,從wait方法中返回然后繼續(xù)執(zhí)行。
注意:有另外一種通知的方法,notify,這個(gè)方法只喚醒一個(gè)線程。因?yàn)閚otify這個(gè)方法不允許你指定將要被喚醒的線程,所以notify方法只適用于大規(guī)模的并行應(yīng)用程序,那種大量的做類似工作的 線程。在這種應(yīng)用程序中,你不需要去關(guān)心哪一個(gè)線程被喚醒。
讓我們用警戒塊來創(chuàng)建一個(gè)生產(chǎn)者——消費(fèi)者應(yīng)用程序。這中應(yīng)用程序在兩個(gè)線程當(dāng)中共享數(shù)據(jù):生產(chǎn)者線程,負(fù)責(zé)創(chuàng)建數(shù)據(jù);消費(fèi)者線程,負(fù)責(zé)消費(fèi)數(shù)據(jù)。兩個(gè)線程用一個(gè)共享的對(duì)象進(jìn)行交流。協(xié)調(diào)是必不可少的:消費(fèi)者線程在生產(chǎn)者傳遞數(shù)據(jù)之前不可以嘗試獲得數(shù)據(jù),生產(chǎn)者在消費(fèi)者還沒有獲得就數(shù)據(jù)之前不可以嘗試去傳遞新數(shù)據(jù)。
在下面的這個(gè)例子中,數(shù)據(jù)時(shí)一系列的文本消息,這些數(shù)據(jù)在Drop對(duì)象中共享。
`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();
}
}`
消費(fèi)者線程在類Producer中進(jìn)行定義,它發(fā)送一系列常見的消息。DONE字符串表示所有的消息已經(jīng)被發(fā)送完成。為了模仿真實(shí)世界中應(yīng)用程序的不可預(yù)知性,生產(chǎn)者線程在發(fā)送消息之間停止隨機(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中定義,它僅僅獲取這些數(shù)據(jù)然后打印出來直到收到DONE字符串。這個(gè)線程也停止隨機(jī)時(shí)間。
`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) {}
}
}
}`
最后,下面是啟動(dòng)生產(chǎn)線程和消費(fèi)著線程的類ProducerConsumerExample。
`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這樣寫是為了展示警戒塊。為了避免重復(fù)造輪子,在去編寫你的共享對(duì)象時(shí)嗎,先去Java Collections Framework 中檢查一下已經(jīng)存在的數(shù)據(jù)結(jié)構(gòu)。如果你想了解更多的信息,請(qǐng)?zhí)D(zhuǎn)到章節(jié)Questions and Exercises
不可變對(duì)象
如果一個(gè)對(duì)象在創(chuàng)建之后,它的狀態(tài)就不可以改變了,那么這個(gè)對(duì)象就是不可變對(duì)象。對(duì)不可變對(duì)象的最大依賴被廣泛認(rèn)為是創(chuàng)建簡(jiǎn)單可靠代碼的合理策略。
不可變對(duì)象在并發(fā)應(yīng)用當(dāng)中特別有用。因?yàn)樗鼈兊臓顟B(tài)無法改變,它們不可能受線程干擾和狀態(tài)不一致的影響。
開發(fā)者經(jīng)常不情愿的去使用不可變對(duì)象,比起更新對(duì)象的開銷它們更擔(dān)心的是創(chuàng)建一個(gè)新的對(duì)象的開銷。創(chuàng)建新對(duì)象的影響經(jīng)常被估高了,這些影響可以被一些高效的不可變對(duì)像抵消。這包括減少由于gc或者為了消除讓那些可變對(duì)象在并發(fā)中運(yùn)行的代碼的開銷。
下面的幾個(gè)小節(jié)從可變的對(duì)象的類當(dāng)中得到不可變對(duì)象的類。通過這么做,讓大家知道這種通用的轉(zhuǎn)換規(guī)則,同時(shí)也展示了一些不可變對(duì)象的優(yōu)點(diǎn)。
一個(gè)同步類的例子
類SynchronizedRGB定義了代表顏色的對(duì)象。每一個(gè)對(duì)象代表一種顏色,由三個(gè)int類型代表顏色的和顏色的名字組成。
`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必須特別小心,避免出現(xiàn)名字和顏色的狀態(tài)不統(tǒng)一的情況。假定,例如,一個(gè)線程執(zhí)行了下面的代碼:
`SynchronizedRGB color =
new SynchronizedRGB(0, 0, 0, "Pitch Black");
...
int myColorInt = color.getRGB(); //Statement 1
String myColorName = color.getName(); //Statement 2`
如果另外一個(gè)線程在語句一和語句二之間調(diào)用了color的set方法,myColorInt的值將和myColorName的值不符。為了避免這種情況發(fā)生,這個(gè)兩條語句必須綁在一起執(zhí)行:
`synchronized (color) {
int myColorInt = color.getRGB();
String myColorName = color.getName();
} `
這一類的不一致只可能在可變的對(duì)象中出現(xiàn),這對(duì)于類SynchronizedRGB的不可變版本來說不是問題。
定義不可變對(duì)象的策略
下面幾條規(guī)則定義了一個(gè)簡(jiǎn)單的創(chuàng)建不可變對(duì)象的策略。不是所有記錄在案的不可變對(duì)象都遵循這些規(guī)則。但這不是指這些類的創(chuàng)建者很粗心——他們也許有更好的理由去保證這些類的實(shí)例在創(chuàng)建之后不會(huì)改變狀態(tài)。然而這些策略需要復(fù)雜的分析,不適合初學(xué)者。
- 不提供改變屬性或者對(duì)象引用屬性的setter方法。
- 所有的屬性都聲明為final和private的。
- 不允許子類重寫方法。最簡(jiǎn)單實(shí)現(xiàn)這個(gè)的方法是聲明類為final的。 一個(gè)更加復(fù)雜的方法是聲明構(gòu)造器是private的,在工廠方法中創(chuàng)建實(shí)例。
- 如果實(shí)例屬性中包含可變對(duì)象,不允許這些對(duì)象被改變:
- 不提供改變這些對(duì)象的方法。
- 不要共享可變對(duì)象的引用。決不存儲(chǔ)傳遞給構(gòu)造器的外部可變的對(duì)象的引用;如果需要,創(chuàng)建副本,存儲(chǔ)副本的引用。同樣,創(chuàng)建你的內(nèi)部可變對(duì)象的引用時(shí),要避免直接使用原來的方法。
將這些策略應(yīng)用到類SynchronizedRGB的結(jié)果如果:
- 在這個(gè)類當(dāng)中有兩個(gè)setter方法。第一個(gè)方法set可以任意改變對(duì)象的狀態(tài),在不可變版本的類當(dāng)中需要?jiǎng)h除。第二個(gè)方法invert可以轉(zhuǎn)化為創(chuàng)建一個(gè)新對(duì)象而不是改變現(xiàn)有的對(duì)象。
- 所有的屬性已經(jīng)是private的了;它們需要進(jìn)一步聲明為final的。
- 將類聲明為final的。
- 只有一個(gè)屬性指向這個(gè)對(duì)象,且這個(gè)對(duì)象本身是不可變的。所以防范改變包含的可變的對(duì)象是不需要的。
下面是改變之后的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);
}
}`
高級(jí)別的并發(fā)對(duì)象
到目前為止,從一開始我們主要集中在講解Java平臺(tái)的部分低級(jí)別的API。對(duì)于一些基本的任務(wù)這些API可以勝任,但是一些更高級(jí)的任務(wù)需要更高級(jí)別的構(gòu)建塊。這對(duì)于充分利用當(dāng)今的多處理器和多核系統(tǒng)的大規(guī)模并發(fā)應(yīng)用來說尤其如此。
在本節(jié)中,我們將介紹Java平臺(tái)5.0版本中引入的一些高級(jí)并發(fā)功能。這些功能中的大多數(shù)都是在新包java.util.concurrent中實(shí)現(xiàn)的。在Java容器框架中也有新的并發(fā)數(shù)據(jù)結(jié)構(gòu)。
- Lock objects支持簡(jiǎn)化許多應(yīng)用程序的鎖定方法。
- Executors定義了一種高級(jí)別的啟動(dòng)和管理線程的API。包java.util.concurrent的Executor的實(shí)現(xiàn)提供了使用于大型應(yīng)用程序的線程池管理。
- 并發(fā)集合使管理大量數(shù)據(jù)變得更容易,可以極大的減少同步的需要。
- 原子變量有最小化同步的特性,且可以幫助避免內(nèi)存一致性錯(cuò)誤。
- 在JDK7中的ThreadLocalRandom類提供了在多線程中高效產(chǎn)生隨機(jī)數(shù)的方式。
鎖對(duì)象
同步代碼依賴一種簡(jiǎn)單的重入鎖。這種鎖極易使用但是也有很多限制。包java.util.concurrent.locks支持更加復(fù)雜的鎖方式。我們不會(huì)去檢查這個(gè)包的詳情,我們主要是集中在基礎(chǔ)的接口Lock
上面。
鎖對(duì)象和同步代碼的隱士鎖一樣工作。和隱士鎖一樣,在同一時(shí)刻,只有一個(gè)線程可以占有鎖對(duì)象。鎖對(duì)象同時(shí)也支持wait或notify原理,通過相關(guān)聯(lián)的Condition對(duì)象實(shí)現(xiàn)。
鎖對(duì)象相比于隱士鎖最大的優(yōu)勢(shì)是鎖對(duì)象有嘗試獲得鎖然后退出的能力。tryLock方法會(huì)立即退出或者在一個(gè)指定的超時(shí)時(shí)間后退出。lockInterruptibly方法在它獲得鎖之前如果另外一個(gè)線程發(fā)送了中斷會(huì)退出。
讓我們來用鎖對(duì)象解決在Liveness章節(jié)中遇到的死鎖問題。A和B通過訓(xùn)練知道什么時(shí)候別人會(huì)鞠躬。我們通過要求我們的朋友對(duì)象在繼續(xù)進(jìn)行鞠躬之前必須獲得兩個(gè)參與者的鎖來模擬這種改進(jìn)。下面是改進(jìn)的類SafeLock的源代碼。為了展示這種方式的多功能性,我們假定A和B非常喜歡他們新的安全的鞠躬的能力,他們不停的像對(duì)方鞠躬。
`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();
}
}`
Executors
Executor的一些接口
包java.util.concurrent定義了3個(gè)executor接口:
- Executor,一個(gè)可以支持啟動(dòng)新任務(wù)的簡(jiǎn)單接口。
- ExecutorService是Executor的子接口,它增加了有助于管理生命周期的功能,包括單個(gè)任務(wù)和執(zhí)行器本身。
- ScheduledExecutorService是ExecutorService的子接口,支持未來和/或定期執(zhí)行任務(wù)。
通常,引用執(zhí)行器對(duì)象的變量被聲明為這三種接口類型之一,而不是執(zhí)行器類類型。
Executor接口
Executor接口提供了一個(gè)簡(jiǎn)單的execute方法,這個(gè)方法被設(shè)計(jì)成代替常見的線程創(chuàng)建方式。如果r是一個(gè)Runnable對(duì)象,且e是一個(gè)Executor對(duì)象,你可以用替換
`(new Thread(r)).start();`
為
`e.execute(r);`
然而execute的定義不太具體。低級(jí)別的方式創(chuàng)建一個(gè)線程然后立即啟動(dòng)它。依賴于Executor的實(shí)現(xiàn),execute方法可以做相同的事情,但是這個(gè)更像是使用一個(gè)已經(jīng)存在的線程去跑r或者是說把r放到一個(gè)等待隊(duì)列中,等到有空閑的工作線程。我們將在章節(jié)Thread Pools中講述工作線程。
ExecutorService接口
ExecutorService接口提供類似于execute的更通用的submit方法。和execute方法一樣,submit方法接受Runnable的對(duì)象,同時(shí)也接受Callable
對(duì)象,這個(gè)對(duì)象允許任務(wù)有返回值。submit方法返回一個(gè)Future
的對(duì)象,這個(gè)對(duì)象用來獲得Callable的返回值,管理Callable和Runnable任務(wù)的狀態(tài)。
ExecutorService
接口也提供方法提交大量的Callable對(duì)象。最后,ExecutorService提供一些管理executor停止執(zhí)行的方法。為了支持立即的停止執(zhí)行,任務(wù)需要正確的處理中斷。
ScheduledExecutorService Interface
ScheduledExecutorService
接口schedule方法,這個(gè)方法可以在指定的延遲之后執(zhí)行Runnable或者是Callable任務(wù)。而且,接口還定義了scheduleAtFixedRate和schedulerWithFixedDelay方法,這些方法可以在指定的時(shí)間間隔內(nèi)重復(fù)執(zhí)行。
線程池
大多數(shù)在包java.util.concurrent中的executor的實(shí)現(xiàn)都使用了線程池,線程池當(dāng)中包含工作線程。這種線程和Runable任務(wù),Callable任務(wù)是不同的,它們經(jīng)常被用來執(zhí)行多任務(wù)。
使用工作線程最小化線程創(chuàng)建所帶來的開銷。線程對(duì)象需要使用大量的內(nèi)存,在大規(guī)模的應(yīng)用中,多線程對(duì)象的分配和釋放需要消耗大量的內(nèi)存。
一種常見的線程池類型是固定大小的線程池。這種類型的線程池當(dāng)中總是保持指定大小的線程數(shù)運(yùn)行著;如果一個(gè)線程在使用當(dāng)中因?yàn)槟承┰虮唤K止了,它將自動(dòng)的被一個(gè)新的線程替代。任務(wù)通過一個(gè)內(nèi)部的隊(duì)列提交到線程池當(dāng)中,當(dāng)提交的任務(wù)數(shù)超過線程數(shù)時(shí),這些超過的任務(wù)會(huì)進(jìn)入到隊(duì)列當(dāng)中去。
固定線程池的一個(gè)重要優(yōu)點(diǎn)是應(yīng)用程序可以優(yōu)雅降級(jí)的使用它。為了理解這個(gè),假設(shè)一個(gè)網(wǎng)頁服務(wù)應(yīng)用程序用單獨(dú)的線程處理每個(gè)http的請(qǐng)求。如果應(yīng)用程序?yàn)槊總€(gè)新的http請(qǐng)求都創(chuàng)建一個(gè)新的線程,緊接著系統(tǒng)會(huì)立即收到很多線程,當(dāng)所有這些線程的開銷超過系統(tǒng)的容量時(shí)應(yīng)用程序會(huì)突然停止響應(yīng)所有的請(qǐng)求。因?yàn)榫€程創(chuàng)建的數(shù)量是有限制的,應(yīng)用程序?qū)⒉荒軌虬凑説ttp請(qǐng)求進(jìn)來的速度響應(yīng)它們,但是應(yīng)用程序會(huì)以自己所能處理的最快速度去響應(yīng)它們。
一個(gè)簡(jiǎn)單的創(chuàng)建固定大小的線程池的方法是調(diào)用java.util.concurrent.Executors
的newFixedThreadPool
工廠方法。這個(gè)類同時(shí)也提供了下列的這些工廠方法:
newCachedThreadPool
方法創(chuàng)建一個(gè)有著可以擴(kuò)展大小的線程池的執(zhí)行器。此執(zhí)行器適用于啟動(dòng)許多短時(shí)間任務(wù)的應(yīng)用程序。newSingleThreadExecutor
方法創(chuàng)建一個(gè)在同一時(shí)刻只執(zhí)行單個(gè)任務(wù)的執(zhí)行器。數(shù)個(gè)工廠方法是上述執(zhí)行器的ScheduledExecutorService數(shù)個(gè)版本。
如果上述executors提供的工廠方法沒有一個(gè)滿足你的要求,創(chuàng)建 java.util.concurrent.ThreadPoolExecutor
或java.util.concurrent.ScheduledThreadPoolExecutor
的實(shí)例將給你更多的選擇。
Fork/Join
Fork/Join是一種實(shí)現(xiàn)了ExecutorService的框架,它可以幫助你充分利用多處理器的優(yōu)勢(shì)。它是為那些可以遞歸的分解成更小的任務(wù)的工作而設(shè)計(jì)的。它的目的是使用所有可用的處理器的能力來增強(qiáng)應(yīng)用的性能。
和其它ExecutorService的實(shí)現(xiàn)一樣,fork/join框架將任務(wù)分配給在線程池中的工作線程。但是fork/join框架的區(qū)別是它使用了work-stealing算法。工作線程在做完自己的任務(wù)之后可以偷其它繁忙的線程的任務(wù)來做。
fork/join框架的核心是類是ForkJoinPool
,它是類AbstractExecutorService的擴(kuò)展。ForkJoinPool實(shí)現(xiàn)了work-stealing算法,它可以執(zhí)行ForkJoinTask
。
基本使用方法
使用fork/join框架的第一步是編寫工作的一段的代碼。你的代碼應(yīng)該和下面的這些代碼類似:
if (my portion of the work is small enough) do the work directly else split my work into two pieces invoke the two pieces and wait for the results
把這段代碼放在ForkJoinTask的子類當(dāng)中,典型的使用更加專業(yè)的類,RecursiveTask
(可以返回結(jié)果)或者是RecursiveAction
。
在你的ForkJoinTask的子類準(zhǔn)備好了之后,創(chuàng)建一個(gè)代表所有的需要做的工作的對(duì)象,然后把它給到ForkJoinPool實(shí)例的invoke方法。
模糊處理
為了幫你理解fork/join框架是如何工作的,看一下下面的例子。假定你要模糊處理一張圖片。一個(gè)整數(shù)類型的數(shù)組代表原始的圖片,每一個(gè)單個(gè)的整數(shù)代表單個(gè)像素的顏色的值。經(jīng)過模糊處理的目標(biāo)圖片也是用與原圖片相同大小的整數(shù)數(shù)組表示的。
模糊處理是通過一次處理原數(shù)組的一個(gè)像素來完成的。每一個(gè)像素取它的周圍的像素的平均值 (紅色、綠色和藍(lán)色分別平均),然后將結(jié)果放到目標(biāo)數(shù)組當(dāng)中。因?yàn)橐粡垐D片是一個(gè)很大的數(shù)組,這樣子處理會(huì)消耗很長(zhǎng)的時(shí)間。你可以使用fork/join框架充分利用多處理器系統(tǒng)的并發(fā)處理能力。下面是一個(gè)可能的實(shí)現(xiàn):
`public class ForkBlur extends RecursiveAction {
private int[] mSource;
private int mStart;
private int mLength;
private int[] mDestination;
// Processing window size; should be odd.
private int mBlurWidth = 15;
public ForkBlur(int[] src, int start, int length, int[] dst) {
mSource = src;
mStart = start;
mLength = length;
mDestination = dst;
}
protected void computeDirectly() {
int sidePixels = (mBlurWidth - 1) / 2;
for (int index = mStart; index < mStart + mLength; index++) {
// Calculate average.
float rt = 0, gt = 0, bt = 0;
for (int mi = -sidePixels; mi <= sidePixels; mi++) {
int mindex = Math.min(Math.max(mi + index, 0),
mSource.length - 1);
int pixel = mSource[mindex];
rt += (float)((pixel & 0x00ff0000) >> 16)
/ mBlurWidth;
gt += (float)((pixel & 0x0000ff00) >> 8)
/ mBlurWidth;
bt += (float)((pixel & 0x000000ff) >> 0)
/ mBlurWidth;
}
// Reassemble destination pixel.
int dpixel = (0xff000000 ) |
(((int)rt) << 16) |
(((int)gt) << 8) |
(((int)bt) << 0);
mDestination[index] = dpixel;
}
}`
...
未完待續(xù)~~~