學(xué)習(xí)Java多線程之線程定義、狀態(tài)和屬性
一 、線程和進(jìn)程
1. 什么是線程和進(jìn)程的區(qū)別:
線程是指程序在執(zhí)行過(guò)程中,能夠執(zhí)行程序代碼的一個(gè)執(zhí)行單元。在java語(yǔ)言中,線程有四種狀態(tài):運(yùn)行 、就緒、掛起和結(jié)束。
進(jìn)程是指一段正在執(zhí)行的程序。而線程有事也被成為輕量級(jí)的進(jìn)程,他得程序執(zhí)行的最小單元,一個(gè)進(jìn)程可以擁有多個(gè)線程,各個(gè)線程之間共享程序的內(nèi)功空間(代碼段、數(shù)據(jù)段和堆空間)及一些進(jìn)程級(jí)的資源(例如打開(kāi)的文件),但是各個(gè)線程都擁有自己的??臻g。
2. 為何要使用多進(jìn)程
在操作系統(tǒng)級(jí)別上來(lái)看主要有以下幾個(gè)方面:
- 使用多線程可以減少程序的響應(yīng)時(shí)間,如果某個(gè)操作和耗時(shí),或者陷入長(zhǎng)時(shí)間的等待,此時(shí)程序講不會(huì)響應(yīng)鼠標(biāo)和鍵盤(pán)等的操作,使用多線程后可以把這個(gè)耗時(shí)的線程分配到一個(gè)單獨(dú)的線程去執(zhí)行,從而使程序具備了更好的交互性。
- 與進(jìn)程相比,線程創(chuàng)建和切換開(kāi)銷更小,同時(shí)多線程在數(shù)據(jù)共享方面效率非常高。
- 多CPU或者多核計(jì)算機(jī)本身就具備執(zhí)行多線程的能力,如果使用單個(gè)進(jìn)程,將無(wú)法重復(fù)利用計(jì)算機(jī)資源,造成資源的巨大浪費(fèi)。在多CPU計(jì)算機(jī)使用多線程能提高CPU的利用率。
- 使用多線程能簡(jiǎn)化程序的結(jié)構(gòu),使程序便于理解和維護(hù)
二、創(chuàng)建線程
多線程的實(shí)現(xiàn)一般有以下三種方法其中前兩種為最常用的方法:
1. 繼承Thread類,重寫(xiě)run()方法
Thread本質(zhì)上也是實(shí)現(xiàn)了Runnable接口的一個(gè)實(shí)例。需要注意的是調(diào)用start()方法后并不是是立即的執(zhí)行多線程的代碼,而是使該線程變?yōu)榭蛇\(yùn)行態(tài),什么時(shí)候運(yùn)行多線程代碼是由操作系統(tǒng)決定的。
以下是主要步驟:
(1)定義Thread類的子類,并重寫(xiě)該類的run方法,該run方法的方法體就代表了線程要完成的任務(wù)。因此把run()方法稱為執(zhí)行體。
(2)創(chuàng)建Thread子類的實(shí)例,即創(chuàng)建了線程對(duì)象。
(3)調(diào)用線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
public class TestThread extends Thread{ public void run() { System.out.println("Hello World"); } public static void main(String[] args) { Thread mThread = new TestThread(); mThread.start(); } }
2. 實(shí)現(xiàn)Runnable接口,并實(shí)現(xiàn)該接口的run()方法
以下是主要步驟:
(1)自定義類并實(shí)現(xiàn)Runnable接口,實(shí)現(xiàn)run()方法。
(2)創(chuàng)建Thread子類的實(shí)例,用實(shí)現(xiàn)Runnable接口的對(duì)象作為參數(shù)實(shí)例化該Thread對(duì)象。
(3)調(diào)用Thread的start()方法來(lái)啟動(dòng)該線程。
public class TestRunnable implements Runnable { public void run() { System.out.println("Hello World"); } } public class TestRunnable { public static void main(String[] args) { TestRunnable mTestRunnable = new TestRunnable(); Thread mThread = new Thread(mTestRunnable); mThread.start(); } }
3. 實(shí)現(xiàn)Callable接口,重寫(xiě)call()方法
Callable接口實(shí)際是屬于Executor框架中的功能類,Callable接口與Runnable接口的功能類似,但提供了比Runnable更強(qiáng)大的功能,主要表現(xiàn)為以下的3點(diǎn):
(1)Callable可以在任務(wù)接受后提供一個(gè)返回值,Runnable無(wú)法提供這個(gè)功能。
(2)Callable中的call()方法可以拋出異常,而Runnable的run()方法不能拋出異常。
(3)運(yùn)行Callable可以拿到一個(gè)Future對(duì)象,F(xiàn)uture對(duì)象表示伊布計(jì)算的結(jié)果,他提供了檢查計(jì)算是否完成的方法。由于線程屬于異步計(jì)算模型,因此無(wú)法從別的線程中得到函數(shù)的返回值,在這種情況下就可以使用Future來(lái)監(jiān)視目標(biāo)線程調(diào)用call()方法的情況,但調(diào)用Future的get()方法以獲取結(jié)果時(shí),當(dāng)前線程就會(huì)阻塞,知道call()方法的返回結(jié)果。
public class TestCallable { //創(chuàng)建線程類 public static class MyTestCallable implements Callable { public String call() throws Exception { retun "Hello World"; } } public static void main(String[] args) { MyTestCallable mMyTestCallable= new MyTestCallable(); ExecutorService mExecutorService = Executors.newSingleThreadPool(); Future mfuture = mExecutorService.submit(mMyTestCallable); try { //等待線程結(jié)束,并返回結(jié)果 System.out.println(mfuture.get()); } catch (Exception e) { e.printStackTrace(); } } }
上述程序的輸出結(jié)果為:Hello World
在這三種方式中,一般推薦實(shí)現(xiàn)Runnable接口的方式,其原因是:首先,Thread類定義了多種方法可以被派生類使用重寫(xiě),但是只有run()方法是必須被重寫(xiě)的,實(shí)現(xiàn)這個(gè)線程的主要功能,這也是實(shí)現(xiàn)Runnable接口需要的方法。其次,一個(gè)類應(yīng)該在他們需要加強(qiáng)或者修改時(shí)才會(huì)被繼承。因此如果沒(méi)有必要重寫(xiě)Thread類的其他方法,那么在這種情況下最好是用實(shí)現(xiàn)Runnable接口的方式。
三、中斷線程
當(dāng)線程的run()方法執(zhí)行方法體中的最后一條語(yǔ)句后,并經(jīng)由執(zhí)行return語(yǔ)句返回時(shí),或者出現(xiàn)在方法中沒(méi)有捕獲的異常時(shí)線程將終止。在java早期版本中有一個(gè)stop方法,其他線程可以調(diào)用它終止線程,但是這個(gè)方法現(xiàn)在已經(jīng)被棄用了。
interrupt方法可以用來(lái)請(qǐng)求終止線程,當(dāng)一個(gè)線程調(diào)用interrupt方法時(shí),線程的中斷狀態(tài)將被置位。這是沒(méi)有個(gè)線程都具有的boolean標(biāo)志,每個(gè)線程都應(yīng)該不時(shí)的檢查這個(gè)標(biāo)志,來(lái)判斷線程是否被中斷。
要想弄清線程是否被置位,可以調(diào)用Thread.currentThread().isInterrupted():
while(!Thread.currentThread().isInterrupted()){ do something }
但是如果一個(gè)線程被阻塞,就無(wú)法檢測(cè)中斷狀態(tài)。這是產(chǎn)生InterruptedException的地方。當(dāng)一個(gè)被阻塞的線程(調(diào)用sleep或者wait)上調(diào)用interrupt方法。阻塞調(diào)用將會(huì)被InterruptedException中斷。
如果每次迭代之后都調(diào)用sleep方法(或者其他可中斷的方法),isInterrupted檢測(cè)就沒(méi)必要也沒(méi)用處了,如果在中斷狀態(tài)被置位時(shí)調(diào)用sleep方法,它不會(huì)休眠反而會(huì)清除這一狀態(tài)并拋出InterruptedException。所以如果在循環(huán)中調(diào)用sleep,不要去檢測(cè)中斷狀態(tài),只需捕獲InterruptedException。
在很多發(fā)布的代碼中會(huì)發(fā)現(xiàn)InterruptedException被抑制在很低的層次上:
void myTask(){ ... try{ sleep(50) }catch(InterruptedException e){ ... } }
不要這樣做,如果不認(rèn)為catch中做一處理有什么好處的話,有兩種合理的選擇:
在catch中調(diào)用Thread.currentThread().interrup()來(lái)設(shè)置中斷狀態(tài)。調(diào)用者可以對(duì)其進(jìn)行檢測(cè)
更好的選擇用throw InterruptedException標(biāo)記你的方法,不采用try語(yǔ)句塊來(lái)捕獲已成。這樣調(diào)用者可以捕獲這個(gè)異常:
void myTask()throw InterruptedException{ sleep(50) }
四、線程的狀態(tài)
(1). 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。
(2). 就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,其他線程調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程池中,變得可運(yùn)行,等待獲取CPU的使用權(quán)。
(3). 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼。
(4). 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分三種:
- 等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中。
- 同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池中。
- 其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。
(5). 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。
五、線程的優(yōu)先級(jí)和守護(hù)線程
1. 線程優(yōu)先級(jí)
在java中,每一個(gè)線程有一個(gè)優(yōu)先級(jí),默認(rèn)情況下,一個(gè)線程繼承它父類的優(yōu)先級(jí)??梢杂胹etPriority方法提高或降低任何一個(gè)線程優(yōu)先級(jí)??梢詫?yōu)先級(jí)設(shè)置在MIN_PRIORITY(在Thread類定義為1)與MAX_PRIORITY(在Thread類定義為10)之間的任何值。線程的默認(rèn)優(yōu)先級(jí)為NORM_PRIORITY(在Thread類定義為5)。
盡量不要依賴優(yōu)先級(jí),如果確實(shí)要用,應(yīng)該避免初學(xué)者常犯的一個(gè)錯(cuò)誤。如果有幾個(gè)高優(yōu)先級(jí)的線程沒(méi)有進(jìn)入非活動(dòng)狀態(tài),低優(yōu)先級(jí)線程可能永遠(yuǎn)也不能執(zhí)行。每當(dāng)調(diào)度器決定運(yùn)行一個(gè)新線程時(shí),首先會(huì)在具有搞優(yōu)先級(jí)的線程中進(jìn)行選擇,盡管這樣會(huì)使低優(yōu)先級(jí)的線程完全餓死。
2. 守護(hù)線程
調(diào)用setDaemon(true);將線程轉(zhuǎn)換為守護(hù)線程。守護(hù)線程唯一的用途就是為其他線程提供服務(wù)。計(jì)時(shí)線程就是一個(gè)例子,他定時(shí)發(fā)送信號(hào)給其他線程或者清空過(guò)時(shí)的告訴緩存項(xiàng)的線程。當(dāng)只剩下守護(hù)線程時(shí),虛擬機(jī)就退出了,由于如果只剩下守護(hù)線程,就沒(méi)必要繼續(xù)運(yùn)行程序了。
另外JVM的垃圾回收、內(nèi)存管理等線程都是守護(hù)線程。還有就是在做數(shù)據(jù)庫(kù)應(yīng)用時(shí)候,使用的數(shù)據(jù)庫(kù)連接池,連接池本身也包含著很多后臺(tái)線程,監(jiān)控連接個(gè)數(shù)、超時(shí)時(shí)間、狀態(tài)等等。
以上就是關(guān)于Java多線程的線程定義、狀態(tài)和屬性,希望對(duì)大家的學(xué)習(xí)有所幫助。
相關(guān)文章
SpringCloud?服務(wù)注冊(cè)中的nacos實(shí)現(xiàn)過(guò)程
這篇文章主要介紹了SpringCloud?服務(wù)注冊(cè)之nacos實(shí)現(xiàn)過(guò)程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03Java設(shè)計(jì)模式七大原則之合成復(fù)用原則詳解
合成復(fù)用原則(Composite Reuse Principle),即盡量使用組合/聚合的方式,而不是使用繼承。本文將為大家具體介紹一下Java設(shè)計(jì)模式七大原則之一的合成復(fù)用原則,需要的可以參考一下2022-02-02Mybatis第三方PageHelper分頁(yè)插件的使用與原理
提到插件相信大家都知道,插件的存在主要是用來(lái)改變或者增強(qiáng)原有的功能,MyBatis中也一樣,下面這篇文章主要給大家介紹了關(guān)于Mybatis第三方PageHelper分頁(yè)插件的使用與原理,需要的朋友可以參考下2022-02-02Java的函數(shù)式接口@FunctionalInterface的使用說(shuō)明
這篇文章主要介紹了Java的函數(shù)式接口@FunctionalInterface的使用說(shuō)明,我們常用的一些接口Callable、Runnable、Comparator等在JDK8中都添加了@FunctionalInterface注解,需要的朋友可以參考下2024-01-01用StopWatch優(yōu)雅替代currentTimeMillis計(jì)算程序執(zhí)行耗時(shí)
別再用System.currentTimeMillis()計(jì)算程序執(zhí)行耗時(shí)了,擁抱StopWatch優(yōu)雅來(lái)優(yōu)雅的計(jì)算,代碼更簡(jiǎn)潔效率更高,本文帶你了解StopWatch的使用2021-09-09