本系列文章將整理到我在GitHub上的《Java面試指南》倉庫猬膨,更多精彩內(nèi)容請到我的倉庫里查看
喜歡的話麻煩點下Star哈
文章同步發(fā)于我的個人博客:
本文是微信公眾號【Java技術(shù)江湖】的《Java并發(fā)指南》其中一篇议慰,本文大部分內(nèi)容來源于網(wǎng)絡(luò)蛔趴,為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯的技術(shù)博客內(nèi)容旷赖,引用其中了一些比較好的博客文章,如有侵權(quán)萧福,請聯(lián)系作者崇摄。
該系列博文會告訴你如何全面深入地學(xué)習(xí)Java并發(fā)技術(shù)擎值,從Java多線程基礎(chǔ),再到并發(fā)編程的基礎(chǔ)知識配猫,從Java并發(fā)包的入門和實戰(zhàn)幅恋,再到JUC的源碼剖析,一步步地學(xué)習(xí)Java并發(fā)編程泵肄,并上手進(jìn)行實戰(zhàn)捆交,以便讓你更完整地了解整個Java并發(fā)編程知識體系,形成自己的知識框架腐巢。
為了更好地總結(jié)和檢驗?zāi)愕膶W(xué)習(xí)成果品追,本系列文章也會提供一些對應(yīng)的面試題以及參考答案。
如果對本系列文章有什么建議冯丙,或者是有什么疑問的話肉瓦,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂胃惜。
1多線程的優(yōu)點
- 資源利用率更好
- 程序設(shè)計在某些情況下更簡單
- 程序響應(yīng)更快
1.1資源利用率更好案例
方式1
從磁盤讀取一個文件需要5秒泞莉,處理一個文件需要2秒。處理兩個文件則需要14秒
1 5秒讀取文件A2 2秒處理文件A3 5秒讀取文件B4 2秒處理文件B5 ---------------------6 總共需要14秒
方式2
從磁盤中讀取文件的時候船殉,大部分的CPU非常的空閑鲫趁。它可以做一些別的事情。通過改變操作的順序利虫,就能夠更好的使用CPU資源挨厚”てВ看下面的順序:
1 5秒讀取文件A2 5秒讀取文件B + 2秒處理文件A3 2秒處理文件B4 ---------------------5 總共需要12秒
總結(jié):多線程并發(fā)效率提高2秒
1.2程序響應(yīng)更快
設(shè)想一個服務(wù)器應(yīng)用,它在某一個端口監(jiān)聽進(jìn)來的請求疫剃。當(dāng)一個請求到來時钉疫,它把請求傳遞給工作者線程(worker thread),然后立刻返回去監(jiān)聽巢价。而工作者線程則能夠處理這個請求并發(fā)送一個回復(fù)給客戶端牲阁。
while(server is active){
listenThread for request
hand request to workerThread
}
這種方式,服務(wù)端線程迅速地返回去監(jiān)聽蹄溉。因此咨油,更多的客戶端能夠發(fā)送請求給服務(wù)端您炉。這個服務(wù)也變得響應(yīng)更快柒爵。
2多線程的代價
2.1設(shè)計更復(fù)雜
多線程一般都復(fù)雜。在多線程訪問共享數(shù)據(jù)的時候赚爵,這部分代碼需要特別的注意棉胀。線程之間的交互往往非常復(fù)雜。不正確的線程同步產(chǎn)生的錯誤非常難以被發(fā)現(xiàn)冀膝,并且重現(xiàn)以修復(fù)唁奢。
2.2上下文切換的開銷
上下文切換當(dāng)CPU從執(zhí)行一個線程切換到執(zhí)行另外一個線程的時候,它需要先存儲當(dāng)前線程的本地的數(shù)據(jù)窝剖,程序指針等麻掸,然后載入另一個線程的本地數(shù)據(jù),程序指針等赐纱,最后才開始執(zhí)行脊奋。
CPU會在一個上下文中執(zhí)行一個線程,然后切換到另外一個上下文中執(zhí)行另外一個線程疙描。
上下文切換并不廉價诚隙。如果沒有必要,應(yīng)該減少上下文切換的發(fā)生起胰。
2.3增加資源消耗
每個線程需要消耗的資源:
CPU久又,內(nèi)存(維持它本地的堆棧),操作系統(tǒng)資源(管理線程)
3競態(tài)條件與臨界區(qū)
當(dāng)多個線程競爭同一資源時效五,如果對資源的訪問順序敏感地消,就稱存在競態(tài)條件。導(dǎo)致競態(tài)條件發(fā)生的代碼區(qū)稱作臨界區(qū)畏妖。
多線程同時執(zhí)行下面的代碼可能會出錯:
public class Counter {
protected long count = 0;
public void add(long value) {
this.count = this.count + value;
}
}
想象下線程A和B同時執(zhí)行同一個Counter對象的add()方法脉执,我們無法知道操作系統(tǒng)何時會在兩個線程之間切換。JVM并不是將這段代碼視為單條指令來執(zhí)行的瓜客,而是按照下面的順序
從內(nèi)存獲取 this.count 的值放到寄存器
將寄存器中的值增加value
將寄存器中的值寫回內(nèi)存
觀察線程A和B交錯執(zhí)行會發(fā)生什么
this.count = 0;
A: 讀取 this.count 到一個寄存器 (0)
B: 讀取 this.count 到一個寄存器 (0)
B: 將寄存器的值加2
B: 回寫寄存器值(2)到內(nèi)存. this.count 現(xiàn)在等于 2
A: 將寄存器的值加3
由于兩個線程是交叉執(zhí)行的适瓦,兩個線程從內(nèi)存中讀出的初始值都是0竿开。然后各自加了2和3,并分別寫回內(nèi)存玻熙。最終的值可能并不是期望的5否彩,而是最后寫回內(nèi)存的那個線程的值,上面例子中最后寫回內(nèi)存可能是線程A嗦随,也可能是線程B
4線程的運行與創(chuàng)建
Java 創(chuàng)建線程對象有兩種方法:
- 繼承 Thread 類創(chuàng)建線程對象
- 實現(xiàn) Runnable 接口類創(chuàng)建線程對象
注意:
在java中列荔,每次程序運行至少啟動2個線程。一個是main線程枚尼,一個是垃圾收集線程贴浙。因為每當(dāng)使用java命令執(zhí)行一個類的時候,實際上都會啟動一個jvm署恍,每一個jvm實際上就是在操作系統(tǒng)中啟動了一個進(jìn)程崎溃。
5線程的狀態(tài)和優(yōu)先級
線程優(yōu)先級1 到 10 ,其中 1 是最低優(yōu)先級盯质,10 是最高優(yōu)先級袁串。
狀態(tài)
- new(新建)
- runnnable(可運行)
- blocked(阻塞)
- waiting(等待)
- time waiting (定時等待)
- terminated(終止)
狀態(tài)轉(zhuǎn)換
線程狀態(tài)流程如下:
- 線程創(chuàng)建后,進(jìn)入 new 狀態(tài)
- 調(diào)用 start 或者 run 方法呼巷,進(jìn)入 runnable 狀態(tài)
- JVM 按照線程優(yōu)先級及時間分片等執(zhí)行 runnable 狀態(tài)的線程囱修。開始執(zhí)行時,進(jìn)入 running 狀態(tài)
- 如果線程執(zhí)行 sleep王悍、wait破镰、join,或者進(jìn)入 IO 阻塞等压储。進(jìn)入 wait 或者 blocked 狀態(tài)
- 線程執(zhí)行完畢后鲜漩,線程被線程隊列移除。最后為 terminated 狀態(tài)
代碼
public class MyThreadInfo extends Thread {
@Override // 可以省略
public void run() {
System.out.println("run");
// System.exit(1);
}
public static void main(String[] args) {
MyThreadInfo thread = new MyThreadInfo();
thread.start();
System.out.println("線程唯一標(biāo)識符:" + thread.getId());
System.out.println("線程名稱:" + thread.getName());
System.out.println("線程狀態(tài):" + thread.getState());
System.out.println("線程優(yōu)先級:" + thread.getPriority());
}
}
結(jié)果:
線程唯一標(biāo)識符:9
線程名稱:Thread-0
run
線程狀態(tài):RUNNABLE