特別說(shuō)明:文章內(nèi)容是《Java并發(fā)編程的藝術(shù)》讀書筆記
Java是一種多線程語(yǔ)言,從誕生開始就內(nèi)置了對(duì)多線程的支持。正確地使用多線程可以顯著提高程序性能鞍历,但過(guò)多地創(chuàng)建線程和對(duì)線程的不當(dāng)管理也很容易造成問(wèn)題毒涧。
線程簡(jiǎn)介
線程定義
現(xiàn)代操作系統(tǒng)在運(yùn)行一個(gè)程序時(shí),會(huì)為其創(chuàng)建一個(gè)進(jìn)程毛仪。例如,啟動(dòng)一個(gè)Java程序芯勘,操作系統(tǒng)就會(huì)創(chuàng)建一個(gè)Java進(jìn)程箱靴。線程是現(xiàn)代操作系統(tǒng)調(diào)度的最小單元,也叫輕量級(jí)進(jìn)程荷愕,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程衡怀,這些線程都擁有各自的計(jì)算器、堆棧和局部變量等屬性安疗,并且能夠訪問(wèn)共享的內(nèi)存變量抛杨。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時(shí)執(zhí)行荐类。
Java程序天生就是多線程程序蝶桶,可以通過(guò)JMX查看一個(gè)普通的Java程序包含那些線程,代碼如下:
public class MutilThread {
public static void main(String[] args) {
// 獲取Java線程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 獲取線程和線程堆棧信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false,false);
// 遍歷線程線程掉冶,僅打印線程ID和線程名稱信息
for(ThreadInfo threadInfo:threadInfos){
System.out.println("["+threadInfo.getThreadId()+"]"+threadInfo.getThreadName());
}
}
}
運(yùn)行結(jié)果如下:
使用多線程的原因
正確使用多線程真竖,總是能夠給開發(fā)人員帶來(lái)顯著的好處脐雪,而使用多線程的原因主要有以下幾點(diǎn):
1、更多的處理器核心
隨著處理器上的核心數(shù)量越來(lái)越多恢共,以及超線程技術(shù)的廣泛運(yùn)用战秋,現(xiàn)在大多數(shù)計(jì)算機(jī)都比以往更加擅長(zhǎng)并行計(jì)算,而處理器性能的提升方式讨韭,也從更高的主頻向更多的核心發(fā)展脂信。
2、更快的響應(yīng)時(shí)間
有時(shí)我們會(huì)編寫一些業(yè)務(wù)邏輯比較復(fù)雜的代碼透硝,例如狰闪,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)濒生、生成訂單快照埋泵、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等。用戶從單擊“訂購(gòu)”按鈕開始罪治,就要等待這些操作全部完成才能看到訂購(gòu)成功的結(jié)果丽声。但是這么多業(yè)務(wù)操作,如何能夠讓其更快地完成呢觉义?
在上面的場(chǎng)景中雁社,可以使用多線程技術(shù),即將數(shù)據(jù)一致性不強(qiáng)的操作派發(fā)給其他線程處理(也可以使用消息隊(duì)列)晒骇,如生成訂單快照霉撵、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請(qǐng)求的線程能夠盡可能快地處理完成洪囤,縮短了響應(yīng)時(shí)間徒坡,提升了用戶體驗(yàn)。
3箍鼓、 更好的編程模型
Java為多線程編程提供了一致的編程模型,使開發(fā)人員能夠更加專注于問(wèn)題的解決呵曹,即為所遇到的問(wèn)題建立合適的模型款咖,而不是絞盡腦汁地考慮如何將其多線程化。
線程優(yōu)先級(jí)
現(xiàn)代操作系統(tǒng)基本采用時(shí)分的形式調(diào)度運(yùn)行的線程奄喂,操作系統(tǒng)會(huì)分出一個(gè)個(gè)時(shí)間片铐殃,線程會(huì)分配到若干時(shí)間片,當(dāng)線程的時(shí)間片用完了就會(huì)發(fā)生線程調(diào)度跨新,并等待著下次分配富腊。線程分配到的時(shí)間片多少也就決定了線程使用處理器資源的多少,而線程優(yōu)先級(jí)就是決定線程需要多或者少分配一些處理器資源的線程屬性域帐。
在Java線程中赘被,通過(guò)一個(gè)整型成員變量priority來(lái)控制優(yōu)先級(jí)是整,優(yōu)先級(jí)的范圍從1~10,在線程構(gòu)建的時(shí)候可以通過(guò)setPriority(int)方法來(lái)修改優(yōu)先級(jí)民假,默認(rèn)優(yōu)先級(jí)是5浮入,優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級(jí)低的線程。
設(shè)置線程優(yōu)先級(jí)時(shí)羊异,針對(duì)頻繁阻塞(休眠或者I/O操作)的線程需要設(shè)置較高優(yōu)先級(jí)事秀,而偏重計(jì)算(需要較多CPU時(shí)間或者偏運(yùn)算)的線程則設(shè)置較低的優(yōu)先級(jí),確保處理器不會(huì)被獨(dú)占野舶。
注意:線程優(yōu)先級(jí)不能作為程序正確性的依賴易迹,因?yàn)椴僮飨到y(tǒng)可以完全不用理會(huì)Java線程對(duì)于優(yōu)先級(jí)的設(shè)定。
線程的狀態(tài)
Java線程在運(yùn)行的生命周期中可能處于下表所示的6種不同的狀態(tài)平道,在給定的一個(gè)時(shí)刻睹欲,線程只能處于其中的一個(gè)狀態(tài)。
狀態(tài)名稱 | 說(shuō)明 |
---|---|
NEW | 初始狀態(tài)巢掺,線程被構(gòu)建句伶,但是還沒有調(diào)用start()方法 |
RUNNABLE | 運(yùn)行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)籠統(tǒng)地稱作“運(yùn)行中” |
BLOCKED | 阻塞狀態(tài)陆淀,表示線程阻塞于鎖 |
WAITING | 等待狀態(tài)考余,表示線程進(jìn)入等待狀態(tài),進(jìn)入該狀態(tài)表示當(dāng)前線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷) |
TIME_WAITING | 超時(shí)等待狀態(tài)轧苫,該狀態(tài)不同于WAITING楚堤,它是可以在指定的時(shí)間自行返回的 |
TERMINATED | 終止?fàn)顟B(tài),表示當(dāng)前線程已經(jīng)執(zhí)行完畢 |
線程在自身的生命周期中含懊,并不是固定地處于某個(gè)狀態(tài)身冬,而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換,Java線程狀態(tài)變遷如下圖:
Java將操作系統(tǒng)中的運(yùn)行和就緒兩個(gè)狀態(tài)合并稱為運(yùn)行狀態(tài)岔乔。阻塞狀態(tài)是線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài)酥筝,但是阻塞在java.concurrent包中Lock接口的線程狀態(tài)卻是等待狀態(tài),因?yàn)閖ava.concurrent包中Lock接口對(duì)于阻塞的實(shí)現(xiàn)均使用了LockSupport類中的相關(guān)方法雏门。
Daemon線程
Daemon線程是一種支持型線程嘿歌,因?yàn)樗饕挥米鞒绦蛑泻笈_(tái)調(diào)度以及支持性工作。當(dāng)一個(gè)Java虛擬機(jī)中不存在非Daemon線程的時(shí)候茁影,Java虛擬機(jī)將會(huì)退出宙帝。可以通過(guò)調(diào)用Thread.setDaemon(true)將線程設(shè)置為Daemon線程募闲。Daemon屬性需要在啟動(dòng)線程之前設(shè)置步脓,不能在啟動(dòng)線程之后設(shè)置。
在構(gòu)建Daemon線程時(shí),不能依靠finally塊中的內(nèi)容來(lái)確保執(zhí)行關(guān)閉或清理資源的邏輯靴患。如下代碼:
public class Daemon {
public static void main(String[] args) {
Thread thread = new Thread(new DeamonRunner(),"DeamonRunner");
thread.setDaemon(true);
thread.start();
}
static class DeamonRunner implements Runnable{
@Override
public void run() {
try {
Thread.sleep(2000l);
} catch (InterruptedException e) {
//
}finally {
System.out.println("DeamonThread finally run.");
}
}
}
}
運(yùn)行Deamon程序仍侥,可以看到在終端或者命令提示符沒有任何輸出。
啟動(dòng)線程
在運(yùn)行線程之前首先要構(gòu)造一個(gè)線程對(duì)象蚁廓,線程對(duì)象在構(gòu)造的時(shí)候需要提供線程所需要的屬性访圃,如線程所屬的線程組、線程優(yōu)先級(jí)相嵌、是否是Daemon線程等信息腿时。
private void init(ThreadGroup g, Runnable target, String name,long stackSize,AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
// 當(dāng)前線程就是該線程的父線程
Thread parent = currentThread();
this.group = g;
// 將daemon、priority屬性設(shè)置為父線程的對(duì)應(yīng)屬性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.name = name.toCharArray();
this.target = target;
setPriority(priority);
// 將父線程的InheritableThreadLocal復(fù)制過(guò)來(lái)
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 分配一個(gè)線程ID
tid = nextThreadID();
}
在上述過(guò)程中饭宾,一個(gè)新構(gòu)造的線程對(duì)象是由其parent線程來(lái)進(jìn)行空間分配的批糟,而child線程繼承了parent是否為Deamon、優(yōu)先級(jí)和加載資源的ContextClassLoader以及可繼承的ThreadLocal看铆,同時(shí)還會(huì)分配一個(gè)唯一的ID來(lái)標(biāo)識(shí)這個(gè)child線程徽鼎。
線程對(duì)象在初始化完成之后,調(diào)用start()方法就可以啟動(dòng)這個(gè)線程弹惦。線程start()方法的含義是:當(dāng)前線程(即parent線程)同步告知Java虛擬機(jī)否淤,只要線程規(guī)劃器空閑,應(yīng)立即啟動(dòng)調(diào)用start()方法的線程棠隐。
啟動(dòng)一個(gè)線程前石抡,最好為這個(gè)線程設(shè)置線程名稱,因?yàn)檫@樣在使用jstack分析程序或者進(jìn)行問(wèn)題排查時(shí)助泽,就會(huì)給開發(fā)人員提供一些提示啰扛,自定義的線程最好能夠起個(gè)名字。
理解中斷
中斷可以理解為線程的一個(gè)標(biāo)識(shí)位屬性嗡贺,它表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作隐解。中斷好比其他線程對(duì)該線程打了個(gè)招呼,其他線程通過(guò)調(diào)用該線程的interrupt()方法對(duì)其進(jìn)行中斷操作诫睬。
線程通過(guò)檢查自身是否被中斷來(lái)進(jìn)行響應(yīng)煞茫,線程通過(guò)方法isInterrupted()來(lái)進(jìn)行判斷是否被中斷,也可以調(diào)用靜態(tài)方法Thread.interrupted()對(duì)當(dāng)前線程的中斷標(biāo)識(shí)位進(jìn)行復(fù)位摄凡。如果該線程已經(jīng)處于終結(jié)狀態(tài)续徽,即使該線程被中斷過(guò),在調(diào)用該線程對(duì)象的isInterrupted()時(shí)依舊會(huì)返回false架谎。
從Java的API中可以看到炸宵,許多聲明拋出InterruptedException的方法(例如Thread.sleep(longmillis)方法)這些方法在拋出InterruptedException之前辟躏,Java虛擬機(jī)會(huì)先將該線程的中斷標(biāo)識(shí)位清除谷扣,然后拋出InterruptedException,此時(shí)調(diào)用isInterrupted()方法將會(huì)返回false。
過(guò)期的suspend()会涎、resume()和stop()
suspend()裹匙、resume()和stop()方法完成了線程的暫停、恢復(fù)和終止工作末秃,而且非掣乓常“人性化”。但是這些API是過(guò)期的练慕,也就是不建議使用的惰匙。
不建議使用的原因主要有:以suspend()方法為例,在調(diào)用后铃将,線程不會(huì)釋放已經(jīng)占有的資源(比如鎖)项鬼,而是占有著資源進(jìn)入睡眠狀態(tài),這樣容易引發(fā)死鎖問(wèn)題劲阎。同樣绘盟,stop()方法在終結(jié)一個(gè)線程時(shí)不保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機(jī)會(huì)悯仙,因此會(huì)導(dǎo)致程序可能工作在不確定狀態(tài)下龄毡。
因?yàn)閟uspend()、resume()和stop()方法帶來(lái)的副作用锡垄,這些方法才被標(biāo)注為不建議使用的過(guò)期方法沦零,而暫停和恢復(fù)操作可以用等待/通知機(jī)制來(lái)替代。
安全地終止線程
中斷操作是一種簡(jiǎn)便的線程間交互方式偎捎,而這種交互方式最適合用來(lái)取消或停止任務(wù)蠢终。除了中斷以外,還可以利用一個(gè)boolean變量來(lái)控制是否需要停止任務(wù)并終止該線程茴她。
public class Shutdown {
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒寻拂,main線程對(duì)CountThread進(jìn)行中斷,使CountThread能夠感知中斷而結(jié)束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒丈牢,main線程對(duì)Runner two進(jìn)行取消祭钉,使CountThread能夠感知on為false而結(jié)束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
main線程通過(guò)中斷操作和cancel()方法均可使CountThread得以終止。這種通過(guò)標(biāo)識(shí)位或者中斷操作的方式能夠使線程在終止時(shí)有機(jī)會(huì)去清理資源己沛,而不是武斷地將線程停止慌核,因此這種終止線程的做法顯得更加安全和優(yōu)雅。
線程間通信
線程開始運(yùn)行申尼,擁有自己的椏遄浚空間,就如同一個(gè)腳本一樣师幕,按照既定的代碼一步一步地執(zhí)行粟按,直到終止诬滩。但是,每個(gè)運(yùn)行中的線程灭将,如果僅僅是孤立地運(yùn)行疼鸟,那么沒有一點(diǎn)兒價(jià)值,或者說(shuō)價(jià)值很少庙曙,如果多個(gè)線程能夠相互配合完成工作空镜,這將會(huì)帶來(lái)巨大的價(jià)值。
volatile和synchronized關(guān)鍵字
Java支持多個(gè)線程同時(shí)訪問(wèn)一個(gè)對(duì)象或者對(duì)象的成員變量捌朴,由于每個(gè)線程可以擁有這個(gè)變量的拷貝(雖然對(duì)象以及成員變量分配的內(nèi)存是在共享內(nèi)存中的吴攒,但是每個(gè)執(zhí)行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執(zhí)行砂蔽,這是現(xiàn)代多核處理器的一個(gè)顯著特性)舶斧,所以程序在執(zhí)行過(guò)程中,一個(gè)線程看到的變量并不一定是最新的察皇。
關(guān)鍵字volatile可以用來(lái)修飾字段(成員變量)茴厉,就是告知程序任何對(duì)該變量的訪問(wèn)均需要從共享內(nèi)存中獲取,而對(duì)它的改變必須同步刷新回共享內(nèi)存什荣,它能保證所有線程對(duì)變量訪問(wèn)的可見性矾缓。
關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來(lái)進(jìn)行使用,它主要確保多個(gè)線程在同一個(gè)時(shí)刻稻爬,只能有一個(gè)線程處于方法或者同步塊中嗜闻,它保證了線程對(duì)變量訪問(wèn)的可見性和排他性。
通過(guò)使用javap工具查看生成的class文件信息來(lái)分析synchronized關(guān)鍵字的實(shí)現(xiàn)細(xì)節(jié)桅锄,代碼如下
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){
m();
}
}
public static synchronized void m(){
}
}
執(zhí)行javap -v Synchronized.class琉雳,部分相關(guān)輸出如下所示:
對(duì)于同步塊的實(shí)現(xiàn)使用了monitorenter和monitorexit指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED來(lái)完成友瘤。無(wú)論采用哪種方式翠肘,其本質(zhì)是對(duì)一個(gè)對(duì)象的監(jiān)視器進(jìn)行獲取,而這個(gè)獲取過(guò)程是排他的辫秧,也就是同一時(shí)刻只能有一個(gè)線程獲取到由synchronized所保護(hù)對(duì)象的監(jiān)視器束倍。
任意一個(gè)對(duì)象都擁有自己的監(jiān)視器,當(dāng)這個(gè)對(duì)象由同步塊或者這個(gè)對(duì)象的同步方法調(diào)用時(shí)盟戏,執(zhí)行方法的線程必須先獲取到該對(duì)象的監(jiān)視器才能進(jìn)入同步塊或者同步方法绪妹,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會(huì)被阻塞在同步塊和同步方法的入口處,進(jìn)入BLOCKED狀態(tài)柿究。
等待/通知機(jī)制
等待/通知機(jī)制是指一個(gè)線程A調(diào)用了對(duì)象O的wait()方法進(jìn)入等待狀態(tài)邮旷,而另一個(gè)線程B調(diào)用了對(duì)象O的notify()或notifyAll()方法,線程A收到通知后從對(duì)象O的wait()方法返回蝇摸,進(jìn)而執(zhí)行后續(xù)操作婶肩。上述兩個(gè)線程對(duì)象O來(lái)完成交互糕簿,而對(duì)象上的wait()和notify/notifyAll()的關(guān)系就如同開關(guān)信號(hào)一樣,用來(lái)完成等待方通知方之間的交互工作狡孔。
等待/通知的相關(guān)方法是任意Java對(duì)象都具備的,這些方法被定義在所有對(duì)象的超類java.lang.Object上蜂嗽。
1苗膝、實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模型,代碼如下:
public class WaitNotify {
private final static int CONTAINER_MAX_LENGTH = 3;
private static Queue<Integer> resources = new LinkedList<Integer>();
//作為synchronized的對(duì)象監(jiān)視器
private static final Object lock = new Object();
/**
* 消息者
*/
static class Consumer implements Runnable {
@Override
public void run() {
synchronized (lock) {
// 不能使用if判斷植旧,防止過(guò)早喚醒
while (resources.isEmpty()) {
try {
// 當(dāng)前釋放鎖辱揭,線程進(jìn)入等待狀態(tài)。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " get number is " + resources.remove());
// 喚醒所有等待狀態(tài)的線程
lock.notifyAll();
}
}
}
/**
* 生產(chǎn)者
*/
static class Producer implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (resources.size() == CONTAINER_MAX_LENGTH) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int number = (int) (Math.random() * 100);
System.out.println(Thread.currentThread().getName() + " produce number is " + number);
resources.add(number);
lock.notifyAll();
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(new Consumer(), "consumer-" + i).start();
}
for (int i = 0; i < 50; i++) {
new Thread(new Producer(), "producer-" + i).start();
}
}
}
調(diào)用wait()病附、notify()以及notifyAll()時(shí)需要注意的細(xì)節(jié)问窃,如下:
使用wait()、notify()和notifyAll()時(shí)需要先對(duì)調(diào)用對(duì)象加鎖完沪。
調(diào)用wait()方法后域庇,線程狀態(tài)由RUNNING變?yōu)閃AITING,并將當(dāng)前線程放置到對(duì)象的等待隊(duì)列覆积。
notify()或notifyAll()方法調(diào)用后听皿,等待線程依舊不會(huì)從wait()返回,需要調(diào)用notify()或notifAll()的線程釋放鎖之后宽档,等待線程才有機(jī)會(huì)從wait()返回尉姨。
notify()方法將等待隊(duì)列中的一個(gè)等待線程從等待隊(duì)列中移到同步隊(duì)列中,而notifyAll()方法則是將等待隊(duì)列中所有的線程全部移到同步隊(duì)列吗冤,被移動(dòng)的線程狀態(tài)由WAITING變?yōu)锽LOCKED又厉。
從wait()方法返回的前提是獲得了調(diào)用對(duì)象的鎖。
2椎瘟、面試題:設(shè)計(jì)一個(gè)程序覆致,啟動(dòng)三個(gè)線程A,B,C,各個(gè)線程只打印特定的字母,各打印10次肺蔚,例如A線程只打印‘A’篷朵。要求在控制臺(tái)依次顯示“ABCABC…”
public class WaitNotify02 {
public static void main(String[] args) {
Print print = new Print(15);
new Thread(print, "A").start();
new Thread(print, "B").start();
new Thread(print, "C").start();
}
private final static Object lock = new Object();
static class Print implements Runnable {
private int max_print;
private int count = 0;
private String str = "A";
public Print(int max_print) {
this.max_print = max_print;
}
@Override
public void run() {
synchronized (lock) {
String name = Thread.currentThread().getName();
while (count < max_print) {
if (str.equals(name)) {
System.out.print(name);
if (str.equals("A")) {
str = "B";
} else if (str.equals("B")) {
str = "C";
} else {
count++;
str = "A";
}
lock.notifyAll();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}