Java從誕生開始就明智地選擇了內(nèi)置對多線程的支持轩性,這使得Java語言相比同一時(shí)期的其他語言具有明顯的優(yōu)勢声登。線程作為操作系統(tǒng)調(diào)度的最小單元,多個(gè)線程能夠同時(shí)執(zhí)行揣苏,這將顯著提升程序性能悯嗓,在多核環(huán)境中表現(xiàn)得更加明顯。但是卸察,過多地創(chuàng)建線程和對線程的不當(dāng)管理也容易造成問題脯厨。本章將著重介紹Java并發(fā)編程的基礎(chǔ)知識(shí),從啟動(dòng)一個(gè)線程到線程間不同的通信方式坑质,最后通過簡單的線程池示例以及應(yīng)用(簡單的Web服務(wù)器)來串聯(lián)本章所介紹的內(nèi)容合武。
線程簡介
什么是線程
現(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)程(Light Weight Process),在一個(gè)進(jìn)程里可以創(chuàng)建多個(gè)線程,這些線程都擁有各自的計(jì)數(shù)器红淡、堆棧和局部變量等屬性不狮,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換在旱,讓使用者感覺到這些線程在同時(shí)執(zhí)行摇零。
一個(gè)Java程序從main()方法開始執(zhí)行,然后按照既定的代碼邏輯執(zhí)行颈渊,看似沒有其他線程參與遂黍,但實(shí)際上Java程序天生就是多線程程序终佛,因?yàn)閳?zhí)行main()方法的是一個(gè)名稱為main的線程俊嗽。下面使用JMX來查看一個(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)容可能不同)牙捉。
[4] Signal Dispatcher? ? // 分發(fā)處理發(fā)送給JVM信號(hào)的線程
[3] Finalizer? ? ? ? ? ? // 調(diào)用對象finalize方法的線程
[2] Reference Handler? ? // 清除Reference的線程
[1] main? ? ? ? ? ? ? ? // main線程竹揍,用戶程序入口
可以看到,一個(gè)Java程序的運(yùn)行不僅僅是main()方法的運(yùn)行邪铲,而是main線程和多個(gè)其他線程的同時(shí)運(yùn)行芬位。
為什么要使用多線程
執(zhí)行一個(gè)簡單的“Hello, World!”,卻啟動(dòng)了那么多的“無關(guān)”線程带到,是不是把簡單的問題復(fù)雜化了昧碉?當(dāng)然不是,因?yàn)檎_使用多線程揽惹,總是能夠給開發(fā)人員帶來顯著的好處被饿,而使用多線程的原因主要有以下幾點(diǎn)。
(1)更多的處理器核心
隨著處理器上的核心數(shù)量越來越多搪搏,以及超線程技術(shù)的廣泛運(yùn)用狭握,現(xiàn)在大多數(shù)計(jì)算機(jī)都比以往更加擅長并行計(jì)算,而處理器性能的提升方式疯溺,也從更高的主頻向更多的核心發(fā)展论颅。如何利用好處理器上的多個(gè)核心也成了現(xiàn)在的主要問題。
線程是大多數(shù)操作系統(tǒng)調(diào)度的基本單元囱嫩,一個(gè)程序作為一個(gè)進(jìn)程來運(yùn)行恃疯,程序運(yùn)行過程中能夠創(chuàng)建多個(gè)線程,而一個(gè)線程在一個(gè)時(shí)刻只能運(yùn)行在一個(gè)處理器核心上挠说。試想一下澡谭,一個(gè)單線程程序在運(yùn)行時(shí)只能使用一個(gè)處理器核心,那么再多的處理器核心加入也無法顯著提升該程序的執(zhí)行效率。相反蛙奖,如果該程序使用多線程技術(shù)潘酗,將計(jì)算邏輯分配到多個(gè)處理器核心上,就會(huì)顯著減少程序的處理時(shí)間雁仲,并且隨著更多處理器核心的加入而變得更有效率仔夺。
(2)更快的響應(yīng)時(shí)間
有時(shí)我們會(huì)編寫一些較為復(fù)雜的代碼(這里的復(fù)雜不是說復(fù)雜的算法,而是復(fù)雜的業(yè)務(wù)邏輯)攒砖,例如缸兔,一筆訂單的創(chuàng)建,它包括插入訂單數(shù)據(jù)吹艇、生成訂單快照惰蜜、發(fā)送郵件通知賣家和記錄貨品銷售數(shù)量等。用戶從單擊“訂購”按鈕開始受神,就要等待這些操作全部完成才能看到訂購成功的結(jié)果抛猖。但是這么多業(yè)務(wù)操作,如何能夠讓其更快地完成呢鼻听?
在上面的場景中财著,可以使用多線程技術(shù),即將數(shù)據(jù)一致性不強(qiáng)的操作派發(fā)給其他線程處理(也可以使用消息隊(duì)列)撑碴,如生成訂單快照撑教、發(fā)送郵件等。這樣做的好處是響應(yīng)用戶請求的線程能夠盡可能快地處理完成醉拓,縮短了響應(yīng)時(shí)間伟姐,提升了用戶體驗(yàn)。
(3)更好的編程模型
Java為多線程編程提供了良好廉嚼、考究并且一致的編程模型玫镐,使開發(fā)人員能夠更加專注于問題的解決,即為所遇到的問題建立合適的模型怠噪,而不是絞盡腦汁地考慮如何將其多線程化恐似。一旦開發(fā)人員建立好了模型,稍做修改總是能夠方便地映射到Java提供的多線程編程模型上傍念。
線程優(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線程中,通過一個(gè)整型成員變量priority來控制優(yōu)先級(jí),優(yōu)先級(jí)的范圍從1~10嘶摊,在線程構(gòu)建的時(shí)候可以通過setPriority(int)方法來修改優(yōu)先級(jí)延蟹,默認(rèn)優(yōu)先級(jí)是5,優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級(jí)低的線程叶堆。設(shè)置線程優(yōu)先級(jí)時(shí)阱飘,針對頻繁阻塞(休眠或者I/O操作)的線程需要設(shè)置較高優(yōu)先級(jí),而偏重計(jì)算(需要較多CPU時(shí)間或者偏運(yùn)算)的線程則設(shè)置較低的優(yōu)先級(jí)虱颗,確保處理器不會(huì)被獨(dú)占沥匈。在不同的JVM以及操作系統(tǒng)上,線程規(guī)劃會(huì)存在差異忘渔,有些操作系統(tǒng)甚至?xí)雎詫€程優(yōu)先級(jí)的設(shè)定高帖,示例代碼清單。
Priority. java
public class Priority {
? ? private static volatile boolean notStart = true;
? ? private static volatile boolean notEnd = true;
? ? public static void main(String[] args) throws Exception {List<Job> jobs = new ArrayList<Job>();
? ? ? ? for (int i = 0; i < 10; i++) {int priority = i < 5 ? Thread.MIN_PRIORITY : Thread.MAX_PRIORITY;
? ? ? ? ? ? Job job = new Job(priority);jobs.add(job);Thread thread = new Thread(job, "Thread:" + i);
? ? ? ? ? ? thread.setPriority(priority);thread.start();}notStart = false;TimeUnit.SECONDS.sleep(10);
? ? ? ? notEnd = false;for (Job job : jobs) {
? ? ? ? ? ? System.out.println("Job Priority : " + job.priority + ",? ? ? ? ? ? ? ? ? ? Count : " + job.jobCount);}}
? ? static class Job implements Runnable {
? ? ? ? private int priority;
? ? ? ? private long jobCount;
? ? ? ? public Job(int priority) {this.priority = priority;}
? ? ? ? public void run() {while (notStart) {Thread.yield();}
? ? ? ? ? ? while (notEnd) {Thread.yield();jobCount++;}}
? ? }
}
運(yùn)行該示例辨萍,在筆者機(jī)器上對應(yīng)的輸出如下棋恼。
Job Priority : 1, Count : 1259592
Job Priority : 1, Count : 1260717
Job Priority : 1, Count : 1264510
Job Priority : 1, Count : 1251897
Job Priority : 1, Count : 1264060
Job Priority : 10, Count : 1256938
Job Priority : 10, Count : 1267663
Job Priority : 10, Count : 1260637
Job Priority : 10, Count : 1261705
Job Priority : 10, Count : 1259967
從輸出可以看到線程優(yōu)先級(jí)沒有生效,優(yōu)先級(jí)1和優(yōu)先級(jí)10的Job計(jì)數(shù)的結(jié)果非常相近锈玉,沒有明顯差距。這表示程序正確性不能依賴線程的優(yōu)先級(jí)高低义起。注意
線程優(yōu)先級(jí)不能作為程序正確性的依賴拉背,因?yàn)椴僮飨到y(tǒng)可以完全不用理會(huì)Java線程對于優(yōu)先級(jí)的設(shè)定。筆者的環(huán)境為:Mac OS X 10.10默终,Java版本為1.7.0_71椅棺,經(jīng)過筆者驗(yàn)證該環(huán)境下所有Java線程優(yōu)先級(jí)均為5(通過jstack查看),對線程優(yōu)先級(jí)的設(shè)置會(huì)被忽略齐蔽。另外两疚,嘗試在Ubuntu 14.04環(huán)境下運(yùn)行該示例,輸出結(jié)果也表示該環(huán)境忽略了線程優(yōu)先級(jí)的設(shè)置含滴。
線程的狀態(tài)
Java線程在運(yùn)行的生命周期中可能處于表4-1所示的6種不同的狀態(tài)诱渤,在給定的一個(gè)時(shí)刻,線程只能處于其中的一個(gè)狀態(tài)谈况。
Java線程的狀態(tài)
由圖可以看到勺美,線程創(chuàng)建之后,調(diào)用start()方法開始運(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í),在沒有獲取到鎖的情況下华蜒,線程將會(huì)進(jìn)入到阻塞狀態(tài)舷蒲。線程在執(zhí)行Runnable的run()方法之后將會(huì)進(jìn)入到終止?fàn)顟B(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接口對于阻塞的實(shí)現(xiàn)均使用了LockSupport類中的相關(guān)方法域滥。
往期精選
程序員必讀的基本功圖書纵柿,你讀了幾本?
轉(zhuǎn)架構(gòu)師你需要準(zhǔn)備哪些面試題目
Java并發(fā)機(jī)制的底層實(shí)現(xiàn)原理 -? synchronized和volatile
史上最適合初學(xué)者入門的Java基礎(chǔ)視頻免費(fèi)放送
Google面試官親授Java面試視頻
計(jì)算機(jī)系統(tǒng)基礎(chǔ)公開課-南京大學(xué)
年薪40w启绰,你需要具備什么技能昂儒。
每周推薦一本書 - 《大型網(wǎng)站技術(shù)架構(gòu)》
每周推薦一本書 - 《spring cloud》