第12章 并發(fā)
????????你可能已經(jīng)很熟悉多任務(wù)(multitasking),這是操作系統(tǒng)的一種能力,看起來可以在同一時(shí)刻運(yùn)行多個(gè)程序狈定。例如,在編輯或下載郵件的同時(shí)可以打印文件吱殉。如今掸冤,人們往往都有多 CPU 的計(jì)算機(jī),但是友雳,并發(fā)執(zhí)行的進(jìn)程數(shù)目并不受限于 CPU 數(shù)目稿湿。操作系統(tǒng)會(huì)為每個(gè)進(jìn)程分配 CPU 時(shí)間片,給人并行處理的感覺押赊。
我們可以在一個(gè)或多個(gè) CPU 的操作系統(tǒng)中同時(shí)運(yùn)行多個(gè)進(jìn)程饺藤,每個(gè)進(jìn)程 CPU 會(huì)分配時(shí)間片,給我們的感覺是這些任務(wù)都可以同時(shí)運(yùn)行流礁。
????????多線程程序在更低一層擴(kuò)展了多任務(wù)的概念:?jiǎn)蝹€(gè)程序看起來在同時(shí)完成多個(gè)任務(wù)涕俗。每個(gè)任務(wù)在一個(gè)線程(thread)中執(zhí)行,線程是控制線程的簡(jiǎn)稱神帅。如果一個(gè)程序可以同時(shí)運(yùn)行多個(gè)線程再姑,則稱這個(gè)程序是多線程的(multithreaded)。
什么樣的程序才能稱得上是多線程的找御,只有能控制多個(gè)線程的應(yīng)用程序才可以元镀。
????????那么,多進(jìn)程與多線程有哪些區(qū)別呢霎桅?本質(zhì)的區(qū)別在于每個(gè)進(jìn)程都擁有自己的一整套變量栖疑,而線程則共享數(shù)據(jù)。這聽起來似乎有些風(fēng)險(xiǎn)滔驶,的確也是這樣遇革,本章稍后將介紹這個(gè)問題。不過揭糕,共享變量使線程之間的通信比進(jìn)程之間的通信更有效萝快、更容易。此外著角,在有些操作系統(tǒng)中杠巡,與進(jìn)程相比較,線程更“輕量級(jí)”雇寇,創(chuàng)建氢拥、撤銷一個(gè)線程比啟動(dòng)新進(jìn)程的開銷要小很多。
也就是說锨侯,進(jìn)程的數(shù)據(jù)是隔離的嫩海,而線程是共享的。
????????在實(shí)際應(yīng)用中囚痴,多線程非常有用叁怪。例如,一個(gè)瀏覽器可以同時(shí)下載幾幅圖片深滚。一個(gè) Web 服務(wù)器需要同時(shí)服務(wù)并發(fā)的請(qǐng)求奕谭。圖形用戶界面(GUI)程序用一個(gè)獨(dú)立的線程從宿主操作環(huán)境收集用戶界面事件涣觉。本章將介紹如何為 Java 應(yīng)用程序添加多線程功能。
多線程可以在很多的場(chǎng)景中應(yīng)用
????????溫馨提示:多線程編程可能會(huì)變得相當(dāng)復(fù)雜血柳。本章涵蓋了應(yīng)用程序員可能需要的所有工具官册。盡管如此,對(duì)于更復(fù)雜的系統(tǒng)級(jí)程序設(shè)計(jì)难捌,建議參看更高級(jí)的參考文獻(xiàn)膝宁,例如,Brian Goetz 等撰寫的《Java Concurrency in Pratice》(Addison-Wesly Professional根吁,2006)员淫。
這里學(xué)習(xí)的內(nèi)容比較基礎(chǔ),學(xué)完這里之后可以看一下比較高級(jí)的書击敌,我目前推薦《Java并發(fā)編程的藝術(shù)》介返。
12.1 什么是線程
????????首先來看一個(gè)使用了兩個(gè)線程的簡(jiǎn)單程序。這個(gè)程序可以在銀行賬戶之間完成資金轉(zhuǎn)賬沃斤。我們使用了一個(gè) Bank 類映皆,它可以存儲(chǔ)給定數(shù)目的賬戶的余額。transfer 方法將一定金額從一個(gè)賬戶轉(zhuǎn)移到另一個(gè)賬戶轰枝。具體實(shí)現(xiàn)見程序清單 12-2捅彻。
這里演示了有兩個(gè)線程的例子,非常簡(jiǎn)單的資金轉(zhuǎn)賬的例子鞍陨。
程序清單 12-1
package threads;
/**
* @author Cay Horstmann
* @version 1.30 2004-08-01
*/
public class ThreadTest {
public static final int DELAY = 10;
public static final int STEPS = 100;
public static final double MAX_AMOUNT = 1000;
public static void main(String[] args) {
Bank bank = new Bank(4, 100000);
Runnable task1 = () -> {
try {
for (int i = 0; i < STEPS; i++) {
double amount = MAX_AMOUNT * Math.random();
bank.transfer(0, 1, amount);
Thread.sleep((long) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Runnable task2 = () -> {
try {
for (int i = 0; i < STEPS; i++) {
double amount = MAX_AMOUNT * Math.random();
bank.transfer(2, 3, amount);
Thread.sleep((long) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task1).start();
new Thread(task2).start();
}
}
程序清單 12-2
package threads;
import java.util.Arrays;
/**
* A bank with a number of bank accounts.
*/
public class Bank {
private final double[] accounts;
/**
* Constructs the bank.
*
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* Transfers money from one account to another.
*
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) {
if (accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
/**
* Gets the sum of all account balances.
*
* @return the total balance
*/
public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
/**
* Gets the number of accounts in the bank.
*
* @return the number of accounts
*/
public int size() {
return accounts.length;
}
}
這里先把代碼放出來步淹,以便理解,原文是將代碼放的很靠后
????????在第一個(gè)線程中诚撵,我們將錢從賬戶 0 轉(zhuǎn)移到賬戶 1缭裆。第二個(gè)線程將錢從賬戶 2 轉(zhuǎn)移到賬戶 3。
兩個(gè)線程分別實(shí)現(xiàn)了什么目標(biāo)寿烟。
????????下面是在一個(gè)單獨(dú)的線程中運(yùn)行一個(gè)任務(wù)的簡(jiǎn)單過程:
- 將執(zhí)行這個(gè)任務(wù)的代碼放在一個(gè)類的 run 方法中澈驼,這個(gè)類要實(shí)現(xiàn) Runnable 接口。Runnable 接口非常簡(jiǎn)單筛武,只有一個(gè)方法:
public interface Runnable {
public abstract void run();
}
????????由于 Runnable 是一個(gè)函數(shù)式接口缝其,可以用一個(gè) lambda 表達(dá)式創(chuàng)建一個(gè)實(shí)例:
Runnable r = () -> { task code };
我一般是使用 IDEA 的 IDE 編輯器,寫的時(shí)候直接將接口 new 出來徘六,將里面要實(shí)現(xiàn)的接口實(shí)現(xiàn)了内边,這時(shí)候 IDE會(huì)提示你可以省略new 的過程,使用 IDE 自動(dòng)功能可以方便我們實(shí)現(xiàn) lambda待锈,不需要去記憶寫法漠其。
- 從這個(gè) Runnable 構(gòu)造一個(gè) Thread 對(duì)象:
Thread t = new Thread(r);
- 啟動(dòng)線程:
t.start();
????????為了建立單獨(dú)的線程來完成轉(zhuǎn)賬,我們只需要把轉(zhuǎn)賬代碼放在一個(gè) Runnable 的 run 方法中,然后啟動(dòng)一個(gè)線程:
Runnable r = () -> {
try {
for (int i = 0; i < 100; i++) {
double amount = MAX_AMOUNT * Math.random();
bank.transfer(0, 1, amount);
Thread.sleep((long) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread t = new Thread(r);
t.start();
上面的代碼只是展示了部分代碼和屎。
????????對(duì)于給定的步驟數(shù)拴驮,這個(gè)線程會(huì)轉(zhuǎn)賬一個(gè)隨機(jī)金額,然后休眠一個(gè)隨機(jī)的延遲時(shí)間柴信。
????????我們要捕獲 sleep 方法有可能拋出的 InterruptException
異常套啤。這個(gè)異常會(huì)在 12.3.1 節(jié)討論。一般來說颠印,中斷用來請(qǐng)求終止一個(gè)線程纲岭。相應(yīng)地抹竹,出現(xiàn) InterruptedException
時(shí)线罕,run 方法會(huì)退出。
????????程序還會(huì)啟動(dòng)第二個(gè)線程窃判,它從賬戶 2 向賬戶 3 轉(zhuǎn)賬钞楼。運(yùn)行這個(gè)程序時(shí),可以得到類似這樣的輸出:
我們這里可以運(yùn)行示例代碼12-1和12-2的代碼
Thread[Thread-1,5,main] 907.97 from 2 to 3 Total Balance: 400000.00
Thread[Thread-0,5,main] 883.06 from 0 to 1 Total Balance: 400000.00
Thread[Thread-1,5,main] 188.52 from 2 to 3 Total Balance: 400000.00
Thread[Thread-0,5,main] 469.69 from 0 to 1 Total Balance: 400000.00
Thread[Thread-0,5,main] 547.20 from 0 to 1 Total Balance: 400000.00
Thread[Thread-0,5,main] 942.84 from 0 to 1 Total Balance: 400000.00
Thread[Thread-0,5,main] 183.58 from 0 to 1 Total Balance: 400000.00
Thread[Thread-1,5,main] 501.09 from 2 to 3 Total Balance: 400000.00
Thread[Thread-1,5,main] 65.10 from 2 to 3 Total Balance: 400000.00
Thread[Thread-0,5,main] 324.28 from 0 to 1 Total Balance: 400000.00
Thread[Thread-1,5,main] 127.08 from 2 to 3 Total Balance: 400000.00
......
????????可以看到袄琳,兩個(gè)線程的輸出是交錯(cuò)的询件,這說明它們?cè)诓l(fā)運(yùn)行。實(shí)際上唆樊,兩個(gè)輸出行交錯(cuò)顯示時(shí)宛琅,輸出有時(shí)會(huì)有些混亂。
這里只是告訴我們逗旁,證明了這兩個(gè)線程是交錯(cuò)運(yùn)行的嘿辟。
????????你要了解的就是這些!現(xiàn)在你已經(jīng)知道了如何并發(fā)地運(yùn)行任務(wù)片效。這一章余下的部分會(huì)介紹如何控制線程之間的交互红伦。
這里告訴我們這里只能證明線程是并行執(zhí)行的,其他要等到后面才能告訴我們淀衣。
????????程序的完整代碼見程序清單 12-1昙读。
程序清單 12-1 已經(jīng)在上面了。
注釋:還可以通過建立 Thread 類的一個(gè)子類來定義線程膨桥,如下所示:
class MyThread extends Thread
{
public void run()
{
task code
}
}
然后可以構(gòu)建這個(gè)子類的一個(gè)對(duì)象蛮浑,并調(diào)用它的 start 方法箱靴。不過唬复,現(xiàn)在不再推薦這種方法。應(yīng)當(dāng)把要并行運(yùn)行的任務(wù)與運(yùn)行機(jī)制解耦合呐芥。如果有多個(gè)任務(wù)介牙,為每個(gè)任務(wù)分別創(chuàng)建一個(gè)單獨(dú)的線程開銷會(huì)太大壮虫。實(shí)際上,可以使用一個(gè)線程池,參見 12.6.2 節(jié)的介紹囚似。
警告:不要調(diào)用 Thread 類或 Runnable 對(duì)象的 run 方法剩拢。直接調(diào)用 run 方法只會(huì)在同一個(gè)線程中執(zhí)行這個(gè)任務(wù)——而沒有啟動(dòng)新的線程。實(shí)際上饶唤,應(yīng)當(dāng)調(diào)用
Thread.start
方法徐伐,這會(huì)創(chuàng)建一個(gè)執(zhí)行 run 方法的新線程。
12.2 線程狀態(tài)
線程可以有如下 6 種狀態(tài):
12.3 線程屬性
????????下面幾節(jié)將討論線程的各種屬性募狂,包括中斷的狀態(tài)办素、守護(hù)線程、未捕獲異常的處理器以及不應(yīng)使用的一些遺留特性祸穷。
12.3.1 中斷線程
????????當(dāng)線程的 run
方法執(zhí)行方法體重最后一條語(yǔ)句后再執(zhí)行 return
語(yǔ)句返回時(shí)性穿,或者出現(xiàn)了方法中沒有捕獲的異常時(shí),線程將終止雷滚。在 Java 的早期版本中需曾,還有一個(gè) stop
方法,其他線程可以調(diào)用這個(gè)方法來終止一個(gè)線程祈远。但是這個(gè)方法現(xiàn)在已經(jīng)被廢棄了呆万。12.4.13 節(jié)將討論它被廢棄的緣由。
這里解釋了如果我們要讓正在執(zhí)行的方法中斷车份,需要什么辦法谋减,我的理解,中斷即結(jié)束扫沼,也就是說不能再恢復(fù)了出爹,這里給我們提供了兩種辦法,第一種就是run 方法體中的代碼完全執(zhí)行完了充甚,這很好理解以政,第二個(gè)就是在run 方法的執(zhí)行中出現(xiàn)了未捕獲的異常,會(huì)導(dǎo)致線程執(zhí)行的中斷伴找。這里暫時(shí)不演示了盈蛮,后面有詳細(xì)的講解。
????????除了已經(jīng)廢棄的 stop 方法技矮,沒有辦法可以強(qiáng)制線程終止抖誉。不過,interrupt 方法可以用來請(qǐng)求終止一個(gè)線程衰倦。
我們沒有辦法強(qiáng)制一個(gè)線程停止運(yùn)行了袒炉,通過外力的方式不行了,但是我們可以請(qǐng)求它終止樊零,當(dāng)然請(qǐng)求了也不一定終止我磁,所以這得看情況了孽文,后面會(huì)介紹具體情況。
????????當(dāng)對(duì)一個(gè)線程調(diào)用 interrupt 方法時(shí)夺艰,就會(huì)設(shè)置線程的中斷狀態(tài)芋哭。這是每個(gè)線程都有的 boolean 標(biāo)志。每個(gè)線程都應(yīng)該不時(shí)地檢查這個(gè)標(biāo)志郁副,以判斷線程是否被中斷减牺。
這個(gè)標(biāo)志是我們自己給當(dāng)前線程或者是我們自己從外面給某個(gè)線程設(shè)置的標(biāo)志,我們的線程在運(yùn)行的時(shí)候存谎,應(yīng)該經(jīng)常主動(dòng)的通過代碼來驗(yàn)證一下這個(gè)標(biāo)志拔疚,以便達(dá)到讓我們的程序自己停止的目的。
????????要想得出是否設(shè)置了中斷狀態(tài)既荚,首先調(diào)用靜態(tài)的 Thread.currentThread
方法獲得當(dāng)前線程稚失,然后調(diào)用 isInterrupted
方法:
我們可以通過
Thread.currentThread
獲取線程,再?gòu)倪@個(gè)線程中獲取isInterrupted
方法來判斷當(dāng)前線程是否被設(shè)置了終止標(biāo)志固以。
while (!Thread.currentThread().isInterrupted() && more work to od) {
do more work
}
????????但是墩虹,如果線程被阻塞嘱巾,就無(wú)法檢查中斷狀態(tài)憨琳。這里就要引入 InterruptedException
異常。當(dāng)在一個(gè)被 sleep
或 wait
調(diào)用阻塞的線程上調(diào)用 interrupt
方法時(shí)旬昭,那個(gè)阻塞調(diào)用(即 sleep
或 wait
調(diào)用)將被一個(gè) InterruptedException
異常中斷篙螟。(有一些阻塞 I/O 調(diào)用不能被中斷,對(duì)此應(yīng)該考慮選擇可中斷的調(diào)用问拘。有關(guān)細(xì)節(jié)請(qǐng)參看卷 2 的第 2 章和第 4 章遍略。)
比如我們的代碼像上面一樣,只有
while
方法體中的代碼執(zhí)行完成后骤坐,才能檢查中斷標(biāo)志绪杏,那代碼很有可能阻塞在while
方法體中,而一直無(wú)法走到while
中的判斷語(yǔ)句中纽绍。如果我們的線程正在被 sleep 或 wait 阻塞蕾久,我們調(diào)用 interrupt 方法的時(shí)候,就會(huì)拋出InterruptedException
異常了拌夏,其實(shí)我們可以看一下代碼僧著,在方法聲明的時(shí)候其實(shí)都聲明拋出InterruptedException
異常的,所以這里可以總結(jié)為障簿,如果我們的線程被一些聲明了InterruptedException
的方法阻塞了盹愚,那么調(diào)用 interrupt 方法,該線程就會(huì)直接拋出InterruptedException
異常了站故。所以皆怕,我們這節(jié)在開頭的時(shí)候說了,如果線程在遇到未捕獲的異常的時(shí)候,會(huì)終止運(yùn)行愈腾,所以這個(gè)拋出InterruptedException
的線程朗兵,如果我們沒有在捕獲這個(gè)異常,好好的處理異常的話顶滩,那么這個(gè)異常就會(huì)終止這個(gè)線程的運(yùn)行了余掖。
????????沒有任何語(yǔ)言要求被中斷的線程應(yīng)當(dāng)終止。中斷一個(gè)線程只是要引起它的注意礁鲁。被中斷的線程可以決定如何響應(yīng)中斷盐欺。某些線程非常重要,所以應(yīng)該處理這個(gè)異常仅醇,然后再繼續(xù)執(zhí)行冗美。但是,更普遍的情況是析二,線程只希望將中斷解釋為一個(gè)終止請(qǐng)求粉洼。這種線程的 run 方法具有如下形式:
Runnable r = () -> {
try {
...
while (!Thread.currentThread().isInterrupted() && more work to do) {
do more work
}
} catch (InterruptedException e) {
// thread was interrupted during sleep or wait
} finally {
cleanup,if required
}
// exiting the run method terminates the thread
}
這意思也就是說,即使我們調(diào)用了 interrupt 函數(shù)叶摄,線程也不是必須要停下來的属韧。這只是為了告訴線程一個(gè)信號(hào)而已。就像上面的代碼展示的那樣蛤吓,即使看到了標(biāo)志是中斷的宵喂,我們也可以不中斷,當(dāng)然也可以直接在代碼中判斷為終止会傲,如果是比較重要的線程锅棕,就必須考慮在調(diào)用了 interrupt 函數(shù)以后會(huì)不會(huì)拋出
InterruptedException
異常了,所以這個(gè)時(shí)候我們應(yīng)該是 catch 住這個(gè)異常淌山,處理好后退出裸燎。
????????如果在每次工作迭代之后都調(diào)用 sleep 方法(或者其他可中斷方法),isInterrupted
檢查既沒有必要也沒有用處泼疑。如果設(shè)置了中斷狀態(tài)德绿,此時(shí)倘若調(diào)用 sleep 方法,它不會(huì)休眠王浴。實(shí)際上脆炎,它會(huì)清除中斷狀態(tài)(!)并拋出 InterruptedException
氓辣。因此秒裕,如果你的循環(huán)調(diào)用了 sleep,不要檢測(cè)中斷狀態(tài)钞啸,而應(yīng)當(dāng)捕獲 InterruptedException
異常几蜻,如下所示:
這里的意思是喇潘,如果我們的線程代碼中有類似會(huì)拋出
InterruptedException
的代碼,就沒必要再去判斷什么isInterrupted
的狀態(tài)了梭稚,因?yàn)楹芸赡苁菦]有用的颖低,還不如去捕獲InterruptedException
異常。
Runnable r = () -> {
try {
...
while (more work to do) {
do more work
Thread.sleep(delay);
}
} catch (InterruptedException e) {
// thread was interrupted during sleep
} finally {
cleanup, if required
}
// exiting the run method terminates the thread
};
注釋:有兩個(gè)非常類似的方法弧烤,
interrupted
和isInterrupted
忱屑。interrupted
方法是一個(gè)靜態(tài)方法,它檢查當(dāng)前線程是否被中斷暇昂。而且莺戒,調(diào)用interrupted
方法會(huì)清除該線程的中斷狀態(tài)。另一方面急波,isInterrupted
方法是一個(gè)實(shí)例方法从铲,可以用來檢查是否有線程被中斷。調(diào)用這個(gè)方法不會(huì)改變中斷狀態(tài)澄暮。
12.4 同步
????????在大多數(shù)實(shí)際的多線程應(yīng)用中名段,兩個(gè)或兩個(gè)以上的線程需要共享對(duì)同一數(shù)據(jù)的存取。如果兩個(gè)線程存取同一個(gè)對(duì)象泣懊,并且每個(gè)線程分別調(diào)用了一個(gè)修改該對(duì)象狀態(tài)的方法伸辟,會(huì)發(fā)生什么呢?可以想見嗅定,這兩個(gè)線程會(huì)相互覆蓋自娩。取決于線程訪問數(shù)據(jù)的次序用踩,可能會(huì)導(dǎo)致對(duì)象被破壞渠退。這種情況稱為競(jìng)態(tài)條件(race condition)。
兩個(gè)線程競(jìng)爭(zhēng)同一塊內(nèi)存脐彩,操作同一塊內(nèi)存的數(shù)據(jù)碎乃,會(huì)產(chǎn)生競(jìng)爭(zhēng)的關(guān)系。
12.4.1 競(jìng)態(tài)條件的一個(gè)例子
????????為了避免多線程破壞共享數(shù)據(jù)惠奸,必須學(xué)習(xí)如何同步存取梅誓。在本節(jié)中,你會(huì)看到如果沒有使用同步會(huì)發(fā)生什么佛南。在下一節(jié)中梗掰,你將會(huì)看到如何同步數(shù)據(jù)存取。
多線程有時(shí)候會(huì)破壞共享數(shù)據(jù)嗅回,如果沒有很好的處理及穗,會(huì)產(chǎn)生問題。
????????在下面的測(cè)試程序中绵载,還是考慮我們模擬的銀行埂陆。與 12.1 節(jié)中的例子不同苛白,我們要隨機(jī)地選擇從哪個(gè)源賬戶轉(zhuǎn)賬到哪個(gè)目標(biāo)賬戶。由于這會(huì)產(chǎn)生問題焚虱,所以下面再來仔細(xì)查看 Bank 類 transfer 方法的代碼购裙。
public void transfer(int from, int to, double amount) {
// CAUTIO: unsafe when called from multiple threads
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
}
程序清單 12-3
package unsynch;
import threads.Bank;
/**
* This program shows data corruption when multiple threads access a data structure.
*
* @author Cay Horstmann
* @version 1.32 2018-04-10
*/
public class UnsynchBankTest {
public static final int NACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < NACCOUNTS; i++) {
int fromAccount = i;
Runnable r = new Runnable() {
@Override
public void run() {
try {
while (true) {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((long) (DELAY * Math.random()));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t = new Thread(r);
t.start();
}
}
}
這里面 Bank 在初始化的時(shí)候,會(huì)為每個(gè)賬戶都分配 1000 塊錢鹃栽,然后循環(huán)運(yùn)行 100 個(gè)線程躏率,因?yàn)檠h(huán)的個(gè)數(shù)和賬戶的個(gè)數(shù)是相等的,每個(gè)線程都基本是隨機(jī)的從一個(gè)賬戶向另外的賬戶轉(zhuǎn)賬民鼓,運(yùn)行可以看到結(jié)果賬戶總額不是總是正確的禾锤。
12.4.2 競(jìng)態(tài)條件詳解
????????上一節(jié)中運(yùn)行了一個(gè)程序,其中有幾個(gè)線程會(huì)更新銀行賬戶余額摹察。一段時(shí)間之后恩掷,不知不覺地出現(xiàn)了錯(cuò)誤,可能有些錢會(huì)丟失供嚎,也可能幾個(gè)賬戶同時(shí)有錢進(jìn)賬黄娘。當(dāng)兩個(gè)線程試圖同時(shí)更新同一個(gè)賬戶時(shí),就會(huì)出現(xiàn)這個(gè)問題克滴。假設(shè)兩個(gè)線程同時(shí)執(zhí)行指令
accounts[to] += amount;
上面運(yùn)行的結(jié)果就可以看出來逼争,同時(shí)運(yùn)行的時(shí)候,賬戶余額是不對(duì)的劝赔。
注釋:實(shí)際上可以查看執(zhí)行這個(gè)類中每一個(gè)語(yǔ)句的虛擬機(jī)字節(jié)碼誓焦。運(yùn)行以下命令
javap -c -v Bank
實(shí)際上執(zhí)行的命令是
javap -c -v Bank.class
這里我建議加上.class
對(duì)
Bank.class
文件進(jìn)行反編譯。例如着帽,代碼行
accounts[to] += amount;
會(huì)轉(zhuǎn)換為下面的字節(jié)碼:
D:\IdeaProjects\untitled1\target\classes\threads>javap -c -v Bank 警告: 文件 .\Bank.class 不包含類 Bank Classfile /D:/IdeaProjects/untitled1/target/classes/threads/Bank.class Last modified 2021年3月19日; size 1442 bytes SHA-256 checksum 81004a4d4033d9f6db32543f79b5fbc2ba904eb575f37a756d8aaf3c59385ee7 Compiled from "Bank.java" public class threads.Bank minor version: 0 major version: 59 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #8 // threads/Bank super_class: #2 // java/lang/Object interfaces: 0, fields: 1, methods: 4, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Fieldref #8.#9 // threads/Bank.accounts:[D #8 = Class #10 // threads/Bank #9 = NameAndType #11:#12 // accounts:[D #10 = Utf8 threads/Bank #11 = Utf8 accounts #12 = Utf8 [D #13 = Methodref #14.#15 // java/util/Arrays.fill:([DD)V #14 = Class #16 // java/util/Arrays #15 = NameAndType #17:#18 // fill:([DD)V #16 = Utf8 java/util/Arrays #17 = Utf8 fill #18 = Utf8 ([DD)V #19 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream; #20 = Class #22 // java/lang/System #21 = NameAndType #23:#24 // out:Ljava/io/PrintStream; #22 = Utf8 java/lang/System #23 = Utf8 out #24 = Utf8 Ljava/io/PrintStream; #25 = Methodref #26.#27 // java/lang/Thread.currentThread:()Ljava/lang/Thread; #26 = Class #28 // java/lang/Thread #27 = NameAndType #29:#30 // currentThread:()Ljava/lang/Thread; #28 = Utf8 java/lang/Thread #29 = Utf8 currentThread #30 = Utf8 ()Ljava/lang/Thread; #31 = Methodref #32.#33 // java/io/PrintStream.print:(Ljava/lang/Object;)V #32 = Class #34 // java/io/PrintStream #33 = NameAndType #35:#36 // print:(Ljava/lang/Object;)V #34 = Utf8 java/io/PrintStream #35 = Utf8 print #36 = Utf8 (Ljava/lang/Object;)V #37 = String #38 // %10.2f from %d to %d #38 = Utf8 %10.2f from %d to %d #39 = Methodref #40.#41 // java/lang/Double.valueOf:(D)Ljava/lang/Double; #40 = Class #42 // java/lang/Double #41 = NameAndType #43:#44 // valueOf:(D)Ljava/lang/Double; #42 = Utf8 java/lang/Double #43 = Utf8 valueOf #44 = Utf8 (D)Ljava/lang/Double; #45 = Methodref #46.#47 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #46 = Class #48 // java/lang/Integer #47 = NameAndType #43:#49 // valueOf:(I)Ljava/lang/Integer; #48 = Utf8 java/lang/Integer #49 = Utf8 (I)Ljava/lang/Integer; #50 = Methodref #32.#51 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #51 = NameAndType #52:#53 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #52 = Utf8 printf #53 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; #54 = String #55 // Total Balance: %10.2f%n #55 = Utf8 Total Balance: %10.2f%n #56 = Methodref #8.#57 // threads/Bank.getTotalBalance:()D #57 = NameAndType #58:#59 // getTotalBalance:()D #58 = Utf8 getTotalBalance #59 = Utf8 ()D #60 = Utf8 (ID)V #61 = Utf8 Code #62 = Utf8 LineNumberTable #63 = Utf8 LocalVariableTable #64 = Utf8 this #65 = Utf8 Lthreads/Bank; #66 = Utf8 n #67 = Utf8 I #68 = Utf8 initialBalance #69 = Utf8 D #70 = Utf8 transfer #71 = Utf8 (IID)V #72 = Utf8 from #73 = Utf8 to #74 = Utf8 amount #75 = Utf8 StackMapTable #76 = Utf8 a #77 = Utf8 sum #78 = Class #12 // "[D" #79 = Utf8 size #80 = Utf8 ()I #81 = Utf8 SourceFile #82 = Utf8 Bank.java { public threads.Bank(int, double); descriptor: (ID)V flags: (0x0001) ACC_PUBLIC Code: stack=3, locals=4, args_size=3 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iload_1 6: newarray double 8: putfield #7 // Field accounts:[D 11: aload_0 12: getfield #7 // Field accounts:[D 15: dload_2 16: invokestatic #13 // Method java/util/Arrays.fill:([DD)V 19: return LineNumberTable: line 18: 0 line 19: 4 line 20: 11 line 21: 19 LocalVariableTable: Start Length Slot Name Signature 0 20 0 this Lthreads/Bank; 0 20 1 n I 0 20 2 initialBalance D public void transfer(int, int, double); descriptor: (IID)V flags: (0x0001) ACC_PUBLIC Code: stack=7, locals=5, args_size=4 0: aload_0 1: getfield #7 // Field accounts:[D 4: iload_1 5: daload 6: dload_3 7: dcmpg 8: ifge 12 11: return 12: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 15: invokestatic #25 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 18: invokevirtual #31 // Method java/io/PrintStream.print:(Ljava/lang/Object;)V 21: aload_0 22: getfield #7 // Field accounts:[D 25: iload_1 26: dup2 27: daload 28: dload_3 29: dsub 30: dastore 31: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 34: ldc #37 // String %10.2f from %d to %d 36: iconst_3 37: anewarray #2 // class java/lang/Object 40: dup 41: iconst_0 42: dload_3 43: invokestatic #39 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 46: aastore 47: dup 48: iconst_1 49: iload_1 50: invokestatic #45 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 53: aastore 54: dup 55: iconst_2 56: iload_2 57: invokestatic #45 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 60: aastore 61: invokevirtual #50 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 64: pop 65: aload_0 66: getfield #7 // Field accounts:[D 69: iload_2 70: dup2 71: daload 72: dload_3 73: dadd 74: dastore 75: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream; 78: ldc #54 // String Total Balance: %10.2f%n 80: iconst_1 81: anewarray #2 // class java/lang/Object 84: dup 85: iconst_0 86: aload_0 87: invokevirtual #56 // Method getTotalBalance:()D 90: invokestatic #39 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double; 93: aastore 94: invokevirtual #50 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 97: pop 98: return LineNumberTable: line 31: 0 line 32: 11 line 34: 12 line 35: 21 line 36: 31 line 37: 65 line 38: 75 line 39: 98 LocalVariableTable: Start Length Slot Name Signature 0 99 0 this Lthreads/Bank; 0 99 1 from I 0 99 2 to I 0 99 3 amount D StackMapTable: number_of_entries = 1 frame_type = 12 /* same */ public double getTotalBalance(); descriptor: ()D flags: (0x0001) ACC_PUBLIC Code: stack=4, locals=8, args_size=1 0: dconst_0 1: dstore_1 2: aload_0 3: getfield #7 // Field accounts:[D 6: astore_3 7: aload_3 8: arraylength 9: istore 4 11: iconst_0 12: istore 5 14: iload 5 16: iload 4 18: if_icmpge 38 21: aload_3 22: iload 5 24: daload 25: dstore 6 27: dload_1 28: dload 6 30: dadd 31: dstore_1 32: iinc 5, 1 35: goto 14 38: dload_1 39: dreturn LineNumberTable: line 47: 0 line 49: 2 line 50: 27 line 49: 32 line 52: 38 LocalVariableTable: Start Length Slot Name Signature 27 5 6 a D 0 40 0 this Lthreads/Bank; 2 38 1 sum D StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 14 locals = [ class threads/Bank, double, class "[D", int, int ] stack = [] frame_type = 248 /* chop */ offset_delta = 23 public int size(); descriptor: ()I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #7 // Field accounts:[D 4: arraylength 5: ireturn LineNumberTable: line 61: 0 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lthreads/Bank; } SourceFile: "Bank.java"
我們可以自行找到 transfer 方法那一段的字節(jié)碼杂伟,我們可以看到對(duì)數(shù)據(jù)的操作是分為很多行分別執(zhí)行的。
這些代碼的含義無(wú)關(guān)緊要仍翰。重要的是這個(gè)增加命令是由多條指令組成的赫粥,執(zhí)行這些指令的線程可以在任何一條指令上被中斷。
????????出現(xiàn)這種破壞的可能性有多大呢予借?在一個(gè)有多個(gè)內(nèi)核的現(xiàn)代處理器上越平,出問題的風(fēng)險(xiǎn)相當(dāng)高。我們將打印語(yǔ)句和更新余額的語(yǔ)句交錯(cuò)執(zhí)行灵迫,以提高觀察到這種問題的概率秦叛。
多個(gè)內(nèi)核的CPU會(huì)放大這種效果,因?yàn)橥聲?huì)有更多的線程執(zhí)行打印輸出可能會(huì)讓執(zhí)行線程陷入等待的幾率增加瀑粥,方便我們觀察挣跋。
????????如果刪除打印語(yǔ)句,出問題的風(fēng)險(xiǎn)會(huì)降低利凑,因?yàn)槊總€(gè)線程在再次休眠之前所做的工作很少浆劲,調(diào)度器不太可能在線程的計(jì)算過程中搶占它的運(yùn)行權(quán)嫌术。但是,產(chǎn)生破壞的風(fēng)險(xiǎn)并沒有完全消失牌借。如果在負(fù)載很重的機(jī)器上運(yùn)行大量線程度气,那么,即使刪除了打印語(yǔ)句膨报,程序依然會(huì)出錯(cuò)磷籍。這種錯(cuò)誤可能幾分鐘、幾小時(shí)或幾天后才出現(xiàn)现柠。坦白地說院领,對(duì)程序員而言,最糟糕的事情莫過于這種不定期地出現(xiàn)錯(cuò)誤够吩。
這里我們可以認(rèn)為的讓風(fēng)險(xiǎn)出現(xiàn)的機(jī)會(huì)大大降低比然,但是不能根本性的解決問題,在并發(fā)度非常高的場(chǎng)景周循,還是會(huì)時(shí)不時(shí)的出現(xiàn)錯(cuò)誤的狀況强法。
????????真正的問題是 transfer 方法可能會(huì)在執(zhí)行到中間被中斷。如果能夠確保線程失去控制之前方法已經(jīng)運(yùn)行完成湾笛,那么銀行賬戶對(duì)象的狀態(tài)就不會(huì)被破壞饮怯。
我們從代碼上看,transfer 是不太可能從中間中斷的嚎研,首先 stop 方法已經(jīng)棄用了蓖墅,我們可以調(diào)用 interrupt 方法,但是我們即使設(shè)置了這個(gè)中斷線程也還可以不中斷临扮,繼續(xù)執(zhí)行论矾,或者正好運(yùn)行到了 sleep 處,那么會(huì)拋出異常停止該線程公条,但是也不會(huì)在 transfer 中間中斷拇囊,我的理解是那種比較暴力的因素,比如說靶橱,oom了,整個(gè) java 進(jìn)程崩潰了路捧,這種情況是有可能出現(xiàn)的关霸,所以在這種情況下如何保證 transfer 不會(huì)從中間中斷就是一個(gè)問題了。
12.4.3 鎖對(duì)象
????????有兩種機(jī)制可防止并發(fā)訪問代碼塊杰扫。Java 語(yǔ)言提供了一個(gè) synchronized
關(guān)鍵字來達(dá)到這一目的队寇,另外 Java 5 引入了 ReentrantLock
類。synchronized 關(guān)鍵字會(huì)自動(dòng)提供一個(gè)鎖以及相關(guān)的“條件”章姓,對(duì)于大多數(shù)需要顯式鎖的情況佳遣,這種機(jī)制功能很強(qiáng)大识埋,也很便利。不過零渐,我們相信在分別了解鎖和條件的內(nèi)容之后窒舟,就能更容易地理解 synchronized 關(guān)鍵字。java.util.concurrent
框架為這些基礎(chǔ)機(jī)制提供了單獨(dú)的類诵盼,有關(guān)內(nèi)容會(huì)在本節(jié)以及 12.4.4 節(jié)解釋惠豺。一旦理解了這些基礎(chǔ),我們會(huì)在 12.4.5 節(jié)介紹 synchronized 關(guān)鍵字风宁。
目前有兩種方式可以防止并發(fā)訪問錯(cuò)誤的產(chǎn)生洁墙,一種是加
synchronized
關(guān)鍵字,一種是ReentrantLock
類戒财。
????????用 ReetrantLock
保護(hù)代碼塊的基本結(jié)構(gòu)如下:
// a ReentrantLock object
myLock.lock();
try {
critical section
} finally {
// make sure the lock is unlocked even if an exception is thrown
myLock.unlock();
}
????????這個(gè)結(jié)構(gòu)確保任何時(shí)刻只有一個(gè)線程進(jìn)入臨界區(qū)热监。一旦一個(gè)線程鎖定了對(duì)象,其他任何線程都無(wú)法通過 lock 語(yǔ)句饮寞。當(dāng)其他線程調(diào)用 lock 時(shí)狼纬,它們會(huì)暫停,直到第一個(gè)線程釋放這個(gè)鎖對(duì)象骂际。
myLock.lock();就是上鎖的意思疗琉,在這里就開始鎖住了資源的訪問了,如果這個(gè)線程沒有釋放鎖歉铝,其他線程是無(wú)法訪問這些資源的盈简。
警告:要把 unlock 操作包括在 finally 字句中,這一點(diǎn)至關(guān)重要太示。如果在臨界區(qū)的代碼拋出一個(gè)異常柠贤,鎖必須釋放。否則类缤,其他線程將永遠(yuǎn)阻塞臼勉。
注釋:使用鎖時(shí),就不能使用 try-with-resources 語(yǔ)句餐弱。首先宴霸,解鎖方法名不是 close。不過膏蚓,即使將它重命名瓢谢,try-with-resources 語(yǔ)句也無(wú)法正常工作。它的首部希望聲明一個(gè)新變量驮瞧。但是如果使用一個(gè)鎖氓扛,你可能想使用多個(gè)線程共享的那個(gè)變量(而不是新變量)。
首先是我們無(wú)法使用 JDK 1.7 引入的 try-with-resources 新特性论笔,其次采郎,我們?cè)谑褂玫臅r(shí)候千所,要保證線程使用的是同一把鎖,如果不是一把鎖蒜埋,鎖就失去了意義淫痰。
????????下面使用一個(gè)鎖來保護(hù) Bank 類的 transfer 方法。
package threads;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* A bank with a number of bank accounts.
*/
public class Bank {
private final double[] accounts;
private Lock bankLock = new ReentrantLock();
/**
* Constructs the bank.
*
* @param n the number of accounts
* @param initialBalance the initial balance for each account
*/
public Bank(int n, double initialBalance) {
accounts = new double[n];
Arrays.fill(accounts, initialBalance);
}
/**
* Transfers money from one account to another.
*
* @param from the account to transfer from
* @param to the account to transfer to
* @param amount the amount to transfer
*/
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
if (accounts[from] < amount) {
return;
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
} finally {
bankLock.unlock();
}
}
/**
* Gets the sum of all account balances.
*
* @return the total balance
*/
public double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
/**
* Gets the number of accounts in the bank.
*
* @return the number of accounts
*/
public int size() {
return accounts.length;
}
}
????????假設(shè)一個(gè)線程調(diào)用了 transfer理茎,但是在執(zhí)行結(jié)束前被搶占黑界。再假設(shè)第二個(gè)線程也調(diào)用了 transfer,由于第二個(gè)線程不能獲得鎖皂林,將在調(diào)用 lock 方法時(shí)被阻塞朗鸠。它會(huì)暫停,必須等待第一個(gè)線程執(zhí)行完 transfer 方法础倍。當(dāng)?shù)谝粋€(gè)線程釋放鎖時(shí)烛占,第二個(gè)線程才能開始運(yùn)行(見圖 12-3)。
也就是說沟启,第一個(gè)線程如果獲取了鎖忆家,第二個(gè)線程就無(wú)法再執(zhí)行鎖代碼塊內(nèi)的程序了,當(dāng)然德迹,這是對(duì)于同一個(gè)對(duì)象而言的芽卿。
????????通常我們可能希望保護(hù)會(huì)更新或檢查共享對(duì)象的代碼塊,從而能確信當(dāng)前操作執(zhí)行完之后其他線程才能使用同一個(gè)對(duì)象胳搞。
如果多個(gè)對(duì)象訪問修改同一塊數(shù)據(jù)卸例,那么我們就需要把它鎖起來。
12.4.4 條件對(duì)象
????????通常肌毅,線程進(jìn)入臨界區(qū)后卻發(fā)現(xiàn)只有滿足了某個(gè)條件之后它才能執(zhí)行筷转。可以使用一個(gè)條件對(duì)象來管理那些已經(jīng)獲得了一個(gè)鎖卻不能做有用工作的線程悬而。在這一節(jié)里呜舒,我們會(huì)介紹 Java 庫(kù)中條件對(duì)象的實(shí)現(xiàn)(由于歷史原因,條件對(duì)象經(jīng)常被稱為條件變量(conditional variable))笨奠。
這里我也無(wú)法正確的理解什么是使用一個(gè)條件來管理那些已經(jīng)獲得了一個(gè)鎖卻不能做有用工作的線程是什么含義袭蝗,可能還要往后看才可以。
????????現(xiàn)在來優(yōu)化銀行的模擬程序艰躺。如果一個(gè)賬戶沒有足夠的資金轉(zhuǎn)賬呻袭,我們不希望從這樣的賬戶轉(zhuǎn)出資金。注意不能使用類似下面的代碼:
if (bank.getBalance(from) >= amount) {
bank.transfer(from, to, amount);
}
之前有個(gè)案例就是轉(zhuǎn)賬腺兴,現(xiàn)在要轉(zhuǎn)賬之前,我們都應(yīng)該要判斷一下賬戶里面是不是還有足夠的資金廉侧,如果沒有資金我們就不應(yīng)該從這樣的賬戶中轉(zhuǎn)出資金了页响。
public double getBalance(int account) { return accounts[account]; }
getBalance 方法的代碼應(yīng)該是與上面的代碼類似篓足,直接獲取數(shù)組中索引位置的浮點(diǎn)數(shù)值,并返回闰蚕。這里跟我們說不能使用這段代碼來判斷栈拖,目前我所理解的是,這段代碼判斷和轉(zhuǎn)出操作不是原子的没陡,會(huì)帶來線程安全問題涩哟。
????????在成功地通過這個(gè)測(cè)試之后,但在調(diào)用 transfer 方法之前盼玄,當(dāng)前線程完全有可能被中斷贴彼。
看來這里與我之前推測(cè)的原因一致
if (bank.getBalance(from) >= amount) {
// thread might be deactivated at this point
bank.transfer(from, to, amount);
}
上面的代碼注釋告訴我們,在判斷完金額以后埃儿,線程可能會(huì)被中斷器仗。
????????在線程再次運(yùn)行前,賬戶余額可能已經(jīng)低于提款金額童番。必須確保在檢查余額與轉(zhuǎn)賬活動(dòng)之間沒有其他線程修改余額精钮。為此,可以使用一個(gè)鎖來保護(hù)這個(gè)測(cè)試和轉(zhuǎn)賬操作:
我們必須保證這個(gè)操作是原子的剃斧,在互聯(lián)網(wǎng)中我們一般使用分布式鎖來實(shí)現(xiàn)轨香,因?yàn)橐话惴?wù)器上都是集群的,光鎖一個(gè)進(jìn)程用處不打幼东,別的進(jìn)程還能執(zhí)行臂容。所以這個(gè)鎖的場(chǎng)景我用的不多,不知道為什么企業(yè)很愛考這個(gè)筋粗,我自己開發(fā)中用的是真不多策橘。分布式鎖用的比較多。