1 概述
下面是維基百科上對進(jìn)程和線程的解釋:
進(jìn)程(英語:process)坐榆,是指計算機(jī)中已運行的程序。進(jìn)程為曾經(jīng)是分時系統(tǒng)的基本運作單位韧掩。在面向進(jìn)程設(shè)計的系統(tǒng)(如早期的UNIX象浑,Linux 2.4及更早的版本)中,進(jìn)程是程序的基本執(zhí)行實體句携;在面向線程設(shè)計的系統(tǒng)(如當(dāng)代多數(shù)操作系統(tǒng)榔幸、Linux 2.6及更新的版本)中,進(jìn)程本身不是基本運行單位务甥,而是線程的容器牡辽。程序本身只是指令、數(shù)據(jù)及其組織形式的描述敞临,進(jìn)程才是程序(那些指令和數(shù)據(jù))的真正運行實例态辛。若干進(jìn)程有可能與同一個程序相關(guān)系,且每個進(jìn)程皆可以同步(循序)或異步(平行)的方式獨立運行⊥δ颍現(xiàn)代計算機(jī)系統(tǒng)可在同一段時間內(nèi)以進(jìn)程的形式將多個程序加載到存儲器中奏黑,并借由時間共享(或稱時分復(fù)用),以在一個處理器上表現(xiàn)出同時(平行性)運行的感覺编矾。同樣的熟史,使用多線程技術(shù)(多線程即每一個線程都代表一個進(jìn)程內(nèi)的一個獨立執(zhí)行上下文)的操作系統(tǒng)或計算機(jī)體系結(jié)構(gòu),同樣程序的平行線程窄俏,可在多CPU主機(jī)或網(wǎng)絡(luò)上真正同時運行(在不同的CPU上)蹂匹。
線程(英語:thread)是操作系統(tǒng)能夠進(jìn)行運算調(diào)度的最小單位。它被包含在進(jìn)程之中凹蜈,是進(jìn)程中的實際運作單位限寞。一條線程指的是進(jìn)程中一個單一順序的控制流,一個進(jìn)程中可以并發(fā)多個線程仰坦,每條線程并行執(zhí)行不同的任務(wù)履植。在Unix System V及SunOS中也被稱為輕量進(jìn)程(lightweight processes),但輕量進(jìn)程更多指內(nèi)核線程(kernel thread)悄晃,而把用戶線程(user thread)稱為線程玫霎。
線程是獨立調(diào)度和分派的基本單位。線程可以為操作系統(tǒng)內(nèi)核調(diào)度的內(nèi)核線程,如Win32線程庶近;由用戶進(jìn)程自行調(diào)度的用戶線程翁脆,如Linux平臺的POSIX Thread;或者由內(nèi)核與用戶進(jìn)程拦盹,如Windows 7的線程鹃祖,進(jìn)行混合調(diào)度。
同一進(jìn)程中的多條線程將共享該進(jìn)程中的全部系統(tǒng)資源普舆,如虛擬地址空間恬口,文件描述符和信號處理等等。但同一進(jìn)程中的多個線程有各自的調(diào)用棧(call stack)沼侣,自己的寄存器環(huán)境(register context)祖能,自己的線程本地存儲(thread-local storage)。
一個進(jìn)程可以有很多線程蛾洛,每條線程并行執(zhí)行不同的任務(wù)养铸。
在多核或多CPU,或支持Hyper-threading的CPU上使用多線程程序設(shè)計的好處是顯而易見轧膘,即提高了程序的執(zhí)行吞吐率钞螟。在單CPU單核的計算機(jī)上,使用多線程技術(shù)谎碍,也可以把進(jìn)程中負(fù)責(zé)I/O處理鳞滨、人機(jī)交互而常被阻塞的部分與密集計算的部分分開來執(zhí)行,編寫專門的workhorse線程執(zhí)行密集計算蟆淀,從而提高了程序的執(zhí)行效率拯啦。
以上的描述還是有些“學(xué)院派”,可能會讓人有些難以理解熔任。做個類比褒链,把計算機(jī)CPU一個核心看成是一個工廠,多核就是多個工廠疑苔,進(jìn)程就可以看做是工廠里的車間甫匹,但由于電力系統(tǒng)的問題,同一時刻只能給一個車間供電惦费,即同一時刻只能有一個車間在進(jìn)行生產(chǎn)工作赛惩,其他車間只能“閑著”,直到電力系統(tǒng)把電輸送過來趁餐,然后接著干活,這個過程可以看做是進(jìn)程的調(diào)度篮绰。而線程可以看做是車間里的一個一個工人后雷,這些工人共同協(xié)作以完成任務(wù),他們共享整個車間的“公共區(qū)域”,例如走道臀突,休息室等(即同一進(jìn)程內(nèi)的線程可以共享線程的“共享資源”)勉抓,但他們每個人都有自己的狀態(tài),即他們只知道自己處于什么狀態(tài)候学,手里的任務(wù)是什么藕筋,并不知道別人的狀態(tài)(即線程具有自己的堆棧,寄存器)梳码,但由于資源有限隐圾,例如只有一臺設(shè)備,所以同一時刻只能有一個工人對機(jī)器進(jìn)行操作掰茶,當(dāng)允許操作時間結(jié)束之后或者發(fā)生意外情況時暇藏,該工人從設(shè)備中撤出來,并且記錄自己工作到哪了濒蒋,然后調(diào)度中心選擇其他工人盐碱,讓其操作設(shè)備,這個過程即線程的調(diào)度沪伙。
1.1 多線程和多進(jìn)程的好處
多線程或者多進(jìn)程對性能的提升是很有幫助的瓮顽,試想一下,如果僅僅是單進(jìn)程围橡,那么在你使用QQ聊天的時候暖混,就無法使用Word等軟件了,有了多線程以及CPU的線程調(diào)度某饰,QQ和Word就好像是在同時運行一樣(但實際上是因為進(jìn)程的切換非常迅速儒恋,人類無法在那么短的時間里做出反應(yīng),所以看起來就好像兩個進(jìn)程在并行的執(zhí)行)黔漂。多線程也是類似的诫尽,只是多線程的并發(fā)是在進(jìn)程內(nèi)部的并發(fā),如果程序大量涉及磁盤訪問或者網(wǎng)絡(luò)傳輸?shù)炔僮骶媸兀嗑€程就可以非常有效的提高效率牧嫉,簡單理解就是”人多力量大“。
雖然多線程和多進(jìn)程能提高效率减途,但并不是一定的酣藻,因為進(jìn)程切換和線程切換也是有一定開銷的,如果多進(jìn)程或者多線程的收益不足以抵消進(jìn)程切換或者線程切換的開銷鳍置,采用多進(jìn)程或者多線程技術(shù)甚至?xí)?dǎo)致性能降低辽剧,得不償失,所以不要一上來就采用多線程或者多進(jìn)程編程税产,應(yīng)該多多測試怕轿,比較偷崩,最終再確定方案。
在Java并發(fā)編程中撞羽,我們主要關(guān)注的線程以及線程間的調(diào)度阐斜,通信以及線程安全等問題,而不太關(guān)注進(jìn)程诀紊,一個Java程序在啟動運行后谒出,就是一個JVM進(jìn)程,進(jìn)程之間的調(diào)度由JVM負(fù)責(zé)邻奠,一般的開發(fā)人員不會涉及到JVM的開發(fā)笤喳。所以,本文包括本系列后續(xù)的文章惕澎,基本上都是圍繞著線程來介紹各種技術(shù)莉测,例如線程池,鎖等唧喉。
2 創(chuàng)建線程的三種方式
2.1 通過繼承Thread的方式來創(chuàng)建線程
這種方式主要是創(chuàng)建一個類捣卤,該類繼承Thread,并重寫其run方法八孝,在使用的時候董朝,創(chuàng)建該類的實例,調(diào)用start方法即可啟動線程干跛,該線程運行的就是run方法里的邏輯子姜,演示代碼如下所示:
public class MyThread extends Thread {
//重寫run()方法
@Override
public void run() {
System.out.println("hello, world! I'm " + Thread.currentThread().getName());
}
}
//測試類
public class Main {
public static void main(String[] args) throws InterruptedException {
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
//等待該線程結(jié)束
myThread1.join();
myThread2.join();
}
}
運行可得到類似如下輸出:
hello, world! I'm Thread-0
hello, world! I'm Thread-1
這時候多運行幾次,會發(fā)現(xiàn)有時候Thrad0在Thread1之前楼入,有時候卻是相反哥捕,這說明線程的執(zhí)行順序并不固定,這取決于線程調(diào)度嘉熊。
2.2 通過實現(xiàn)Runnable接口來創(chuàng)建線程
除了繼承Thread類之外遥赚,還可以通過實現(xiàn)Runnable接口來創(chuàng)建線程,Runnable接口只有一個抽象方法阐肤,即public abstract void run();實現(xiàn)Runnable接口的類必須實現(xiàn)該抽象方法凫佛,使用的時候創(chuàng)建Runnable實例,然后將其傳入Thread類的構(gòu)造函數(shù)中孕惜,最后再調(diào)用start方法啟動線程即可愧薛,下面是演示代碼:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("hello, world! I'm " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable);
Thread thread2 = new Thread(myRunnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
輸出結(jié)果和之前一樣,不再多說衫画。
2.3 通過實現(xiàn)Callable來創(chuàng)建線程
這種方式需要結(jié)合FutureTask來實現(xiàn)毫炉,因其相對其他兩種方式比較復(fù)雜,所以說一下整個步驟:
- 創(chuàng)建一個類削罩,實現(xiàn)Callable接口并實現(xiàn)其call方法碘箍,該方法有返回值遵馆,返回值類型是Callable的泛型參數(shù)的類型,如果不指定丰榴,返回類型可以是Object。
- 結(jié)合FutureTask秆撮,將剛剛創(chuàng)建的Callable實現(xiàn)類實例傳遞給構(gòu)造函數(shù)四濒,此時可以獲取到FutureTask實例。
- 因為FutureTask實現(xiàn)了Runnable接口职辨,所以可以傳入Thread類的構(gòu)造函數(shù)中盗蟆,然后獲取Thread類實例,并啟動舒裤。
- 最后可以調(diào)用FutureTask.get()方法取得call方法的返回值喳资,該方法是阻塞的,如果任務(wù)未完成腾供,調(diào)用該方法的線程會被阻塞仆邓,知道任務(wù)完成。
下面是演示代碼:
public class MyCallable implements Callable<Long> {
@Override
public Long call() throws Exception {
System.out.println("hello, world! I'm " + Thread.currentThread().getName());
//返回線程id
return Thread.currentThread().getId();
}
}
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyCallable myCallable = new MyCallable();
//創(chuàng)建兩個任務(wù)
FutureTask<Long> task1 = new FutureTask<>(myCallable);
FutureTask<Long> task2 = new FutureTask<>(myCallable);
//分別把兩個任務(wù)交給不同的線程
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
//調(diào)用get()方法獲取返回值
System.out.println(task1.get());
System.out.println(task2.get());
}
}
這里的FutureTask是JDK1.5才有的東西伴鳖,提供異步的特性节值,關(guān)于異步,本文不會涉及榜聂,后續(xù)會有文章詳細(xì)介紹異步相關(guān)的內(nèi)容搞疗。
2.4 三種方式比較
這三種方式我個人更傾向于第二種,即實現(xiàn)Runnable接口的方式须肆,在Java8中匿乃,使用lambda可以不用顯式的創(chuàng)建Runnable實現(xiàn)類,使得代碼更加簡潔豌汇,編碼效率更高幢炸。如下所示:
Runnable runnable = () -> {
System.out.println("hello, world! I'm " + Thread.currentThread().getName());
};
而第一種方式因為需要繼承Thread類,所以就不能繼承其他類了瘤礁,這可能影響類的擴(kuò)展性阳懂,不是很推薦。
第三種方式一般在異步編程中使用柜思,是三種方式中最復(fù)雜的岩调,如果沒有異步的需求,一般也不使用這種方式赡盘。
3 Thread類
Thread類是非常重要的類号枕,它是對Java線程的抽象,提供了豐富的操控線程(其實操控空間著實不大)的API陨享,例如sleep()葱淳,join()钝腺,start(),isAlive()等赞厕。Thread類的很多方法都是native方法艳狐,即本地方法,其實現(xiàn)不是用Java語言實現(xiàn)的皿桑,而是用C++語言實現(xiàn)的(對于HotSpot虛擬機(jī)毫目,其他的虛擬機(jī)可能不是用的C++),所以在JDK里無法查看源碼诲侮,需要拿到虛擬機(jī)源碼才能看到這些實現(xiàn)镀虐。所以這里主要是介紹幾個常用的方法的功能,不涉及源碼分析沟绪。
3.1 sleep()方法
sleep()方法即睡眠的意思刮便,其參數(shù)是一個long類型的值,表示毫秒數(shù)绽慈,調(diào)用該方法的線程會停止工作恨旱,直到設(shè)置的時間結(jié)束,在停止工作的過程中久信,不會放棄持有的鎖(如果之前有獲取鎖的話)窖杀,這是和Object.wati()方法的主要區(qū)別之一。下面是一個演示代碼:
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Runnable runnable = () -> {
System.out.println("sleep 5s");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello, world! I'm " + Thread.currentThread().getName());
};
Thread thread = new Thread(runnable);
thread.start();
thread.join();
}
}
運行一下程序裙士,發(fā)現(xiàn)先輸出了sleep 5s的字符入客,過了大概5s之后,才輸出hello, world! I'm***的字符腿椎。這就是sleep()的功能桌硫,很簡單,不多說了啃炸。
3.2 join()方法
在以上的示例代碼中铆隘,我都使用了join()方法,該方法的功能是這樣的:該方法調(diào)用的所在線程需要等待工作線程執(zhí)行完畢才能繼續(xù)執(zhí)行南用。
有點繞口膀钠,什么意思呢移国?例如現(xiàn)在我在main方法里調(diào)用了thread1的join方法显晶,此時main線程就是該方法調(diào)用的所在線程,而工作線程就是thread1次乓,所以此時的結(jié)果就是main線程需要等待thread1執(zhí)行完畢筑公,才能繼續(xù)往下執(zhí)行雳窟。
在上面的示例代碼中,我之所以調(diào)用這個方法匣屡,目的是讓main線程等待工作線程執(zhí)行完畢封救,否則可能無法看到工作線程的輸出拇涤。
3.2 中斷相關(guān)方法
與中斷相關(guān)的方法主要有兩個:interrupt()和isInterrupted()。其中interrupt()的功能是將調(diào)用該方法的線程的中斷標(biāo)志位置位誉结,而isInterrupted()的功能就是檢查線程的中斷標(biāo)志位鹅士,當(dāng)中斷標(biāo)志位被置位時,該方法會返回true搓彻。下面的示例代碼演示了如何使用這兩個方法:
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Runnable runnable = () -> {
try {
for (int i = 0; i < 5000000; i++) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("interrupt occur!!");
throw new InterruptedException();
}
}
System.out.println("is it execute this statement?");
} catch (InterruptedException e) {
//在中斷標(biāo)志位再次置位如绸,即表示重新中斷該線程,方便上層的代碼處理
Thread.currentThread().interrupt();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(10);
thread.interrupt(); //向thread線程發(fā)送中斷請求
}
}
在上面的例子中旭贬,我們在main線程調(diào)用了thread線程的interrupt()方法,表示向thread線程發(fā)送中斷請求搪泳,在thread線程的邏輯是輪詢檢查當(dāng)前線程的中斷標(biāo)志位(調(diào)用了isInterrupted()方法)稀轨,當(dāng)檢查到中斷標(biāo)志位的時候,就拋出一個InterruptedException異常岸军,否則會繼續(xù)執(zhí)行System.out.println("is it execute this statement?");該異常被catch捕獲到奋刽,在catch里我們將線程的中斷標(biāo)志位再次置位的目的是將異常傳播給上層代碼,因為在run方法里無法直接拋出異常艰赞,即不能這樣做:
public void run() throws InterruptedException {
//....
}
而如果僅僅是catch了佣谐,而不做一些事情的話,就只是“生吞異撤窖”而已狭魂,這不是一個好的處理方式,比較好的處理方式就交給上層代碼處理党觅,但又無法直接拋出異常雌澄,就只能重新將線程中斷標(biāo)志位置位了,上層代碼可以檢查中斷標(biāo)志位杯瞻,然后在上層處理該異常镐牺。
關(guān)于中斷相關(guān)的知識,建議參考計算機(jī)組成原理或者操作系統(tǒng)相關(guān)資料魁莉。
4 線程狀態(tài)
在Thread類里有一個枚舉內(nèi)部類睬涧,叫State,如下所示:
//省略了注釋
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
該類定義了6個枚舉實例旗唁,表示線程的6個狀態(tài)畦浓,下圖是6種狀態(tài)的狀態(tài)轉(zhuǎn)換圖:
該圖來自一篇博客,我會在最后標(biāo)識出來逆皮。
4.1 New
New即初始狀態(tài)宅粥,當(dāng)一個線程被創(chuàng)建時(即new一個線程實例),線程就處于該狀態(tài)电谣。
4.2 RUNNABLE
RUNNABLE即可運行狀態(tài)秽梅,注意該狀態(tài)其實表示兩個狀態(tài)抹蚀,即就緒狀態(tài)和運行狀態(tài)!這是很關(guān)鍵的企垦,因為我相信很多朋友都看過操作系統(tǒng)相關(guān)的書籍环壤,在操作系統(tǒng)中,它將就緒狀態(tài)和運行狀態(tài)用兩個不同的字段表示钞诡,而Java中僅用一個字段表示郑现,我到現(xiàn)在也不知道為什么Java要這樣做,也許是為了實現(xiàn)方便荧降?
4.3 BLOCKED
當(dāng)需要進(jìn)入臨界區(qū)(被鎖鎖住的區(qū)域)時接箫,如果線程沒有爭搶到鎖資源,就進(jìn)入了BLOCKED阻塞狀態(tài)朵诫。
4.4 WAITING
等待狀態(tài)辛友,調(diào)用Object.wait()、Thread.join()或者LockSupport.park()方法的時候會進(jìn)入該狀態(tài)剪返,該狀態(tài)會放棄鎖資源(如果之前有獲取到鎖資源的話)废累。
4.5 TIMED_WAITING
超時等待狀態(tài),調(diào)用Object.wait(long)脱盲、Thread.sleep()邑滨、Thread.join(long)或者LockSupport.parkNanos和LockSupport.parkUntil方法的時候會進(jìn)入該狀態(tài),該狀態(tài)和WAITING的最主要區(qū)別就是該狀態(tài)有一個超時時間钱反,當(dāng)過了設(shè)置的時間掖看,線程會自動的從該狀態(tài)中“蘇醒”過來。
4.6 TERMINATED
即中止?fàn)顟B(tài)诈铛,當(dāng)線程執(zhí)行完畢之后乙各,會切換到該狀態(tài),然后再“死去”幢竹。
這里的6個狀態(tài)并不一定就和操作系統(tǒng)的線程狀態(tài)一一對應(yīng)耳峦,在開始的時候,我就說過Thread類是Java對操作系統(tǒng)線程的一個抽象焕毫,直接的目的是為了提供方便使用的API蹲坷,而不是簡單的把線程一對一映射過來。
5 守護(hù)線程
Java中有兩種類型的線程:普通用戶線程邑飒,守護(hù)線程循签。守護(hù)線程的作用就是“守護(hù)”,通俗的講就是“保姆”疙咸。只要JVM存在一個非守護(hù)線程县匠,那么守護(hù)線程都不會停止,只有當(dāng)JVM所有的非守護(hù)線程都停止時,守護(hù)線程才會隨著JVM停止而停止乞旦。創(chuàng)建守護(hù)線程和創(chuàng)建普通線程是一樣的贼穆,只是在創(chuàng)建完之后調(diào)用Thread.setDaemon(true);即可。下面是一個守護(hù)線程的示例:
public class Main {
public static void main(String[] args) {
Thread userThread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("user thread finished!");
});
Thread daemonThread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
System.out.println("damon thread : " + i);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true);
userThread.start();
daemonThread.start();
}
}
運行一下兰粉,輸出結(jié)果大致如下所示:
daemon thread : 0
daemon thread : 1
daemon thread : 2
daemon thread : 3
daemon thread : 4
daemon thread : 5
daemon thread : 6
daemon thread : 7
user thread finished!
從輸出上看出故痊,daemon thread只運行了7次循環(huán)體,而代碼中設(shè)置的是100次循環(huán)玖姑,那為什么沒有執(zhí)行完畢就結(jié)束了呢愕秫?因為user thread 結(jié)束了,main線程也隨之結(jié)束焰络,還有一些系統(tǒng)線程也隨著結(jié)束戴甩,JVM不會等待守護(hù)線程執(zhí)行完畢,所以看到的現(xiàn)象就好像守護(hù)線程被打斷一樣闪彼。
這里有個概念上的問題等恐,上面說的“只要JVM存在一個非守護(hù)線程,那么守護(hù)線程都不會停止”其實并不準(zhǔn)確备蚓,守護(hù)線程也是線程,只有當(dāng)守護(hù)線程的任務(wù)是那種“永遠(yuǎn)沒有盡頭”的任務(wù)囱稽,上述的說法才會成立郊尝,否則守護(hù)線程執(zhí)行完畢,線程照樣會退出战惊。所以流昏,大多數(shù)守護(hù)線程的業(yè)務(wù)邏輯都是諸如“定時任務(wù),“定時調(diào)度”吞获,”觸發(fā)式任務(wù)“等况凉,例如GC線程就是守護(hù)線程。
注意各拷!setDaemon()方法需要在start()方法之前調(diào)用刁绒,否則會拋出java.lang.IllegalThreadStateException異常。
5 小結(jié)
本文簡單介紹了進(jìn)程和線程烤黍,對于Java開發(fā)者來說知市,更關(guān)注的是線程,所以本文沒有過多的涉及進(jìn)程速蕊,如果想要了解更多關(guān)于進(jìn)程的知識嫂丙,建議參考操作系統(tǒng)經(jīng)典書籍中關(guān)于進(jìn)程的章節(jié)。之后還介紹了創(chuàng)建線程的幾種方式以及Thread類的幾個常用方法规哲,最后還說了一下Java線程的6個狀態(tài)以及其狀態(tài)轉(zhuǎn)換跟啤。本文是Java并發(fā)編程系列的第一篇,所以涉及的知識點不多,希望讀者理解隅肥。