了解Java并發(fā)編程基礎(chǔ)!超詳細(xì)!
點(diǎn)擊藍(lán)色“程序員的時(shí)光 ”關(guān)注我 ,標(biāo)注“星標(biāo)”,及時(shí)閱讀最新技術(shù)文章

寫在前面:
小伙伴兒們,大家好!今天來(lái)學(xué)習(xí)Java并發(fā)編程基礎(chǔ),作為面試必問(wèn)的知識(shí)點(diǎn),來(lái)深入了解一波!
思維導(dǎo)圖:

1,什么是進(jìn)程和線程?
1.1,進(jìn)程
程序:程序由指令和數(shù)據(jù)組成,但這些指令要運(yùn)行,數(shù)據(jù)要讀寫,就必須將指令加載至CPU,數(shù)據(jù)加載至內(nèi)存。在指令運(yùn)行過(guò)程中還需要用到磁盤、網(wǎng)絡(luò)等設(shè)備。進(jìn)程就是用來(lái)加載指令、管理內(nèi)存、管理IO的。 進(jìn)程:當(dāng)一個(gè)程序被運(yùn)行,從磁盤加載這個(gè)程序的代碼至內(nèi)存,這時(shí)就開(kāi)啟了一個(gè)進(jìn)程。 理解:進(jìn)程就可以視為程序的一個(gè)實(shí)例。大部分程序可以運(yùn)行多個(gè)實(shí)例進(jìn)程(例如記事本,瀏覽器等),也有的程序只能啟動(dòng)一個(gè)實(shí)例進(jìn)程(例如網(wǎng)易云音樂(lè))。
1.2,線程
現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級(jí)進(jìn)程。 在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的程序計(jì)數(shù)器、堆棧和局部變量等屬性,并且能夠訪問(wèn)共享的內(nèi)存變量。 一個(gè)線程就是一個(gè)指令流,將指令流中的一條條指令以一定的順序交給 CPU 執(zhí)行。Java 中,線程作為最小調(diào)度單位,進(jìn)程作為資源分配的最小單位。
Java程序天生就是多線程程序,因?yàn)閳?zhí)行main()方法的是一個(gè)名稱為main的線程。下面使用JMX來(lái)查看一個(gè)普通的Java程序包含哪些線程,代碼如下。
public class MultiThread {
public static void main(String[] args) {
// 獲取Java線程管理MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要獲取同步的monitor和synchronizer信息,僅獲取線程和線程堆棧信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍歷線程信息,僅打印線程ID和線程名稱信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.
getThreadName());
}
}
}
上述程序輸出如下(輸出內(nèi)容可能不同,不用太糾結(jié)下面每個(gè)線程的作用,只用知道 main 線程執(zhí)行 main 方法即可):
[6] Monitor Ctrl-Break //這個(gè)是在idea中特有的線程,eclipse并不會(huì)產(chǎn)生
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分發(fā)處理給 JVM 信號(hào)的線程
[3] Finalizer //調(diào)用對(duì)象 finalize 方法的線程
[2] Reference Handler //清除 reference 線程
[1] main //main線程,程序入口
可以看到,一個(gè)Java程序的運(yùn)行不僅僅是main()方法的運(yùn)行,而是main線程和多個(gè)其他線程的同時(shí)運(yùn)行。
1.3,二者對(duì)比
這個(gè)是Java內(nèi)存區(qū)域圖,我們可以從JVM的角度來(lái)理解線程和進(jìn)程之間的關(guān)聯(lián)。

可以看出,在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的程序計(jì)數(shù)器、堆棧和局部變量等屬性,并且能夠訪問(wèn)共享的內(nèi)存變量。
總結(jié):
進(jìn)程基本上相互獨(dú)立的,而線程存在于進(jìn)程內(nèi),是進(jìn)程的一個(gè)子集 線程通信相對(duì)簡(jiǎn)單,因?yàn)樗鼈児蚕磉M(jìn)程內(nèi)的內(nèi)存,一個(gè)例子是多個(gè)線程可以訪問(wèn)同一個(gè)共享變量 線程更輕量,線程上下文切換成本一般上要比進(jìn)程上下文切換低
2,并行和并發(fā)
并發(fā):同一時(shí)間段,多個(gè)任務(wù)都在執(zhí)行 (單位時(shí)間內(nèi)不一定同時(shí)執(zhí)行); 并行:?jiǎn)挝粫r(shí)間內(nèi),多個(gè)任務(wù)同時(shí)執(zhí)行。
單核 cpu 下,線程實(shí)際還是 串行執(zhí)行 的。操作系統(tǒng)中有一個(gè)組件叫做任務(wù)調(diào)度器,將 cpu 的時(shí)間片(windows 下時(shí)間片最小約為 15 毫秒)分給不同的程序使用,只是由于 cpu 在線程間(時(shí)間片很短)的切換非常快,人類感覺(jué)是同時(shí)運(yùn)行的 。總結(jié)為一句話就是:微觀串行,宏觀并行 , 一般會(huì)將這種線程輪流使用 CPU 的做法稱為并發(fā)。
舉個(gè)例子:
家庭主婦做飯、打掃衛(wèi)生、照顧孩子,她一個(gè)人輪流交替做這多件事,這時(shí)就是并發(fā); 家庭主婦雇了個(gè)保姆,她們一起這些事,這時(shí)既有并發(fā),也有并行(這時(shí)會(huì)產(chǎn)生競(jìng)爭(zhēng),例如鍋只有一口,一 個(gè)人用鍋時(shí),另一個(gè)人就得等待); 雇了3個(gè)保姆,一個(gè)專做飯、一個(gè)專打掃衛(wèi)生、一個(gè)專照顧孩子,互不干擾,這時(shí)是并行;
3,Java里面的線程
3.1,創(chuàng)建和運(yùn)行線程的3種方式
繼承 Thread 類
覆寫父類中的 run() 方法,新線程類創(chuàng)建線程
public class Thread1 extends Thread{
//重寫父類中的run()方法
@Override
public void run() {
System.out.println("這是第一個(gè)線程");
}
public static void main(String[] args) {
Thread1 t1=new Thread1();
t1.start();
}
}實(shí)現(xiàn) Runnable 接口
實(shí)現(xiàn)接口中的 run() 方法,Thread 類創(chuàng)建線程。把線程和任務(wù)(要執(zhí)行的代碼)分開(kāi);
Thread代表線程,Runnable代表可運(yùn)行的任務(wù);
public class Thread2 implements Runnable{
//重寫父類中的run()方法
@Override
public void run() {
System.out.println("這是第二個(gè)線程");
}
public static void main(String[] args) {
//任務(wù)t2
Thread2 t2=new Thread2();
//線程t
Thread t=new Thread(t2);
t.start();
}
}FutureTask 類構(gòu)造創(chuàng)建方法體
3.2,為什么要使用多線程呢?
開(kāi)銷成本低: 線程可以?作是輕量級(jí)的進(jìn)程,是程序執(zhí)?的最?單位,線程間的切換和調(diào)度的成本遠(yuǎn)遠(yuǎn)?于進(jìn)程。另外,多核 CPU 時(shí)代意味著多個(gè)線程可以同時(shí)運(yùn)?,這減少了線程上下?切換的開(kāi)銷。 并發(fā)能力強(qiáng): 現(xiàn)在的系統(tǒng)動(dòng)不動(dòng)就要求百萬(wàn)級(jí)甚?千萬(wàn)級(jí)的并發(fā)量,?多線程并發(fā)編程正是開(kāi)發(fā)?并發(fā)系統(tǒng)的基礎(chǔ),利?好多線程機(jī)制可以??提?系統(tǒng)整體的并發(fā)能?以及性能。 提高CPU利用率:假如我們要計(jì)算?個(gè)復(fù)雜的任務(wù),我們只??個(gè)線程的話,CPU中只會(huì)?個(gè) CPU核?被利?到,?創(chuàng)建多個(gè)線程就可以讓多個(gè) CPU 核?被利?到,這樣就提?了 CPU 的利?,這樣提高了并行性能,也就是提高了CPU利用率。
3.3,線程的狀態(tài)和生命周期
Java線程在運(yùn)行的生命周期中可能處于下表所示的6中不同狀態(tài),在給定的時(shí)刻中,線程只能處于其中一個(gè)狀態(tài)。

線程在自身的生命周期中, 并不是固定地處于某個(gè)狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進(jìn)行切換,Java線程狀態(tài)變遷如圖示。

初始狀態(tài)(NEW)
用new創(chuàng)建一個(gè)線程對(duì)象,借助實(shí)現(xiàn)Runnable接口和繼承Thread類都可以得到一個(gè)線程類,此時(shí)線程進(jìn)入初始狀態(tài)。
運(yùn)行狀態(tài)(RUNNABLE)
就緒(READY)狀態(tài):調(diào)用線程的start()方法可以啟動(dòng)線程。當(dāng)線程啟動(dòng)時(shí),線程就進(jìn)入就緒狀態(tài)。此時(shí),線程將進(jìn)入線程隊(duì)列排隊(duì),等待CPU 服務(wù),這表明它已經(jīng)具備了運(yùn)行條件。運(yùn)行中狀態(tài)(RUNNING狀態(tài)):線程調(diào)度程序從可運(yùn)行池中選擇一個(gè)線程作為當(dāng)前線程時(shí)線程所處的狀態(tài),這也是線程進(jìn)入運(yùn)行狀態(tài)的唯一的一種方式。此時(shí),自動(dòng)調(diào)用該線程對(duì)象的run()方法。run()方法定義了該線程的操作和功能。
由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用
start()方法后開(kāi)始運(yùn)行,線程這時(shí)候處于 READY(就緒) 狀態(tài)。就緒狀態(tài)的線程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。阻塞狀態(tài)(BLOCKED)
阻塞狀態(tài)是線程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài)。
等待狀態(tài)(WAITING)
處于這種狀態(tài)的線程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無(wú)限期等待的狀態(tài)。
超時(shí)等待(TIMED_WAITING)
進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而超時(shí)等待狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,也是超時(shí)時(shí)間到達(dá)時(shí)將會(huì)返回到運(yùn)行狀態(tài)。
終止?fàn)顟B(tài)
當(dāng)線程的run()方法完成時(shí),或者主線程的main()方法完成時(shí),我們就認(rèn)為它終止了。線程一旦終止了,就不能復(fù)生。
線程創(chuàng)建之后,調(diào)用start()方法開(kāi)始運(yùn)行。當(dāng)線程執(zhí)行wait()方法之 后,線程進(jìn)入等待狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài),而超時(shí)等待狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,也就是超時(shí)時(shí)間到達(dá)時(shí)將會(huì)返回到運(yùn)行狀態(tài)。當(dāng)線程調(diào)用同步方法時(shí),在沒(méi)有獲取到鎖的情況下,線程將會(huì)進(jìn)入到阻塞狀態(tài)。線程在執(zhí)行
Runnable的run()方法之后將會(huì)進(jìn)入到終止?fàn)顟B(tài)。
聊完了Java線程狀態(tài),另外,我們?cè)賮?lái)聊一聊操作系統(tǒng)進(jìn)程狀態(tài)。由于這兩者很相似,所以很容易會(huì)混淆。

進(jìn)程一般有5種狀態(tài):
創(chuàng)建狀態(tài)(new):進(jìn)程正在被創(chuàng)建,尚未達(dá)到就緒狀態(tài)。 就緒狀態(tài)(ready):進(jìn)程已處于準(zhǔn)備運(yùn)?狀態(tài),即進(jìn)程獲得了除了處理器之外的?切所需資源, ?旦得到處理器資源(處理器分配的時(shí)間?)即可運(yùn)?。 運(yùn)行狀態(tài)(running):進(jìn)程正在處理器上運(yùn)?(單核 CPU 下任意時(shí)刻只有?個(gè)進(jìn)程處于運(yùn)?狀態(tài))。 阻塞狀態(tài)(waiting):?稱為等待狀態(tài),進(jìn)程正在等待某?事件?暫停運(yùn)?如等待某資源為可?或等待 IO 操作完成。即使處理器空閑,該進(jìn)程也不能運(yùn)?。 結(jié)束狀態(tài)(terminated):進(jìn)程正在從系統(tǒng)中消失。或出現(xiàn)錯(cuò)誤,或被系統(tǒng)終止,進(jìn)入終止?fàn)顟B(tài)。無(wú)法再執(zhí)行
3.4,使用多線程會(huì)存在什么問(wèn)題?
從多線程的設(shè)計(jì)原則中可以看到,多線程雖然并發(fā)能力強(qiáng)、CPU利用率高,但是因?yàn)槠浯嬖趯?duì)共享和可變狀態(tài)的資源進(jìn)行訪問(wèn),所以存在一定的線程安全問(wèn)題。并發(fā)編程的?的就是為了能提?程序的執(zhí)?效率提?程序運(yùn)?速度,但是并發(fā)編程也會(huì)遇到很多問(wèn)題,?如:內(nèi)存泄漏、上下?切換、死鎖還有受限于硬件和軟件的資源閑置問(wèn)題。
3.5,什么是上下文切換?
上下文:每個(gè)任務(wù)運(yùn)行前,CPU 都需要知道任務(wù)從哪里加載、又從哪里開(kāi)始運(yùn)行,這就涉及到 CPU 寄存器和程序計(jì)數(shù)器(PC):
CPU 寄存器是 CPU 內(nèi)置的容量小、但速度極快的內(nèi)存;程序計(jì)數(shù)器會(huì)存儲(chǔ) CPU 正在執(zhí)行的指令位置,或者即將執(zhí)行的指令位置。這兩個(gè)是 CPU 運(yùn)行任何任務(wù)前都必須依賴的環(huán)境,因此叫做 CPU 上下文。
多線程編程中一般線程的個(gè)數(shù)都大于 CPU 核心的個(gè)數(shù),而一個(gè) CPU 核心在任意時(shí)刻只能被一個(gè)線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個(gè)線程分配時(shí)間片并輪轉(zhuǎn)的形式。當(dāng)一個(gè)線程的時(shí)間片用完的時(shí)候就會(huì)重新處于就緒狀態(tài)讓給其他線程使用,這個(gè)過(guò)程就屬于一次上下文切換。
概括來(lái)說(shuō)就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時(shí)間片切換到另一個(gè)任務(wù)之前會(huì)先保存自己的狀態(tài),以便下次再切換回這個(gè)任務(wù)時(shí),可以再加載這個(gè)任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過(guò)程就是一次上下文切換。
這就像我們同時(shí)讀兩本書,當(dāng)我們?cè)谧x一本英文的技術(shù)書時(shí),發(fā)現(xiàn)某個(gè)單詞不認(rèn)識(shí),于是便打開(kāi)中英文字典,但是在放下英文技術(shù)書之前,大腦必須先記住這本書讀到了多少頁(yè)的第 多少行,等查完單詞之后,能夠繼續(xù)讀這本書。這樣的切換是會(huì)影響讀書效率的,同樣上下文 切換也會(huì)影響多線程的執(zhí)行速度。
上下文切換通常是計(jì)算密集型的。也就是說(shuō),它需要相當(dāng)可觀的處理器時(shí)間,在每秒幾十上百次的切換中,每次切換都需要納秒量級(jí)的時(shí)間。所以,上下文切換對(duì)系統(tǒng)來(lái)說(shuō)意味著消耗大量的 CPU 時(shí)間,事實(shí)上,可能是操作系統(tǒng)中時(shí)間消耗最大的操作。
Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時(shí)間消耗非常少。
3.6,如何減少上下文切換?
減少上下文切換的方法有無(wú)鎖并發(fā)編程、CAS算法、使用最少線程和使用協(xié)程。
無(wú)鎖并發(fā)編程:多線程競(jìng)爭(zhēng)鎖時(shí),會(huì)引起上下文切換,所以多線程處理數(shù)據(jù)時(shí),可以用一 些辦法來(lái)避免使用鎖,如將數(shù)據(jù)的ID按照Hash算法取模分段,不同的線程處理不同段的數(shù)據(jù)。 CAS算法:Java的Atomic包使用CAS算法來(lái)更新數(shù)據(jù),而不需要加鎖。 使用最少線程:避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多線程來(lái)處理,這 樣會(huì)造成大量線程都處于等待狀態(tài)。 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換。
3.7,什么是線程死鎖?
死鎖指多個(gè)線程在執(zhí)行過(guò)程中,因爭(zhēng)奪資源而造成的互相等待的現(xiàn)象,在無(wú)外力作用的情況下,這些線程會(huì)一直相互等待而無(wú)法繼續(xù)運(yùn)行下去,如下圖所示:

如上圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時(shí)都想申請(qǐng)對(duì)方的資源,所以這兩個(gè)線程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)。
那么為什么會(huì)產(chǎn)生死鎖呢?學(xué)過(guò)操作系統(tǒng)的應(yīng)該都知道,死鎖的產(chǎn)生必須具備四個(gè)條件:互斥條件,請(qǐng)求和保持條件,不可剝奪條件,循環(huán)等待條件。下面通過(guò)一個(gè)例子來(lái)說(shuō)明線程死鎖。
public class DeadLock {
//創(chuàng)建資源1和資源2
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 A").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "線程 B").start();
}
}
運(yùn)行結(jié)果:
Thread[線程 A,5,main]get resource1
Thread[線程 B,5,main]get resource2
Thread[線程 A,5,main]waiting get resource2
Thread[線程 B,5,main]waiting get resource1
從輸出結(jié)果可知,線程調(diào)度器先調(diào)度了線程A,也就是把CPU資源分配給了線程A,線程A使用 synchronized (resource1) 獲得 resource1 的監(jiān)視器鎖,然后通過(guò)Thread.sleep(1000);讓線程A休眠1s是為了讓線程B得到CPU資源然后執(zhí)行獲取到resource2 的監(jiān)視器鎖。線程A和線程B休眠結(jié)束了都開(kāi)始企圖請(qǐng)求獲取對(duì)方的資源,然后這兩個(gè)線程就會(huì)陷入相互等待的狀態(tài),這也就產(chǎn)生了死鎖。
3.8,如何避免死鎖?
前面說(shuō)到,死鎖產(chǎn)生必須具備四個(gè)條件,我們對(duì)其破壞就可以避免死鎖。
互斥條件:指線程對(duì)已獲取到的資源進(jìn)行排它性使用,該資源任意時(shí)刻只由一個(gè)線程占用;
這個(gè)條件無(wú)法破壞,因?yàn)橛面i本來(lái)就是想讓資源之間排斥的。
請(qǐng)求和保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不釋放;
一次性申請(qǐng)所有資源即可。
不可剝奪條件:線程已獲得的資源在未使用完之前不能被其他線程強(qiáng)行剝奪,只有自己使用完畢后才釋放資源;
占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到,可以主動(dòng)釋放它占有的資源。
循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系
按照申請(qǐng)資源的有序性原則來(lái)預(yù)防。按某一順序申請(qǐng)資源,釋放資源則反序釋放,破壞循環(huán)等待條件。
造成死鎖的原因其實(shí)和申請(qǐng)資源的順序有很大關(guān)系,使用資源申請(qǐng)的有序性原則就可以避免死鎖。
我們對(duì)上面線程B的代碼進(jìn)行修改:
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 B").start();
運(yùn)行結(jié)果:
Thread[線程 A,5,main]get resource1
Thread[線程 A,5,main]waiting get resource2
Thread[線程 A,5,main]get resource2
Thread[線程 B,5,main]get resource1
Thread[線程 B,5,main]waiting get resource2
Thread[線程 B,5,main]get resource2
分析下上面的代碼為什么避免的死鎖的發(fā)生?
假如線程A和線程B同時(shí)執(zhí)行到了synchronized (resource1),只有一個(gè)線程可以獲取到resource1上的監(jiān)視器鎖。假如線程A獲取到了,那么線程B就會(huì)被阻塞而不會(huì)再去獲取resource1,然后線程A再去獲取resource2的監(jiān)視器鎖,可以獲取到;這時(shí)候線程A釋放了對(duì)resource1、resource2的監(jiān)視器鎖的占用,線程B獲取到就可以執(zhí)行了。這樣就破壞了循環(huán)等待條件,因此避免了死鎖。
參考文獻(xiàn):
Java并發(fā)編程之美
JavaGuide面試突擊
微信搜索公眾號(hào)《程序員的時(shí)光》
好了,今天就先分享到這里了,下期繼續(xù)給大家?guī)?lái)Java線程相關(guān)內(nèi)容!更多干貨、優(yōu)質(zhì)文章,歡迎關(guān)注我的原創(chuàng)技術(shù)公眾號(hào)~
點(diǎn)個(gè)[在看],是對(duì)我最大的支持!

