面試中如何答好:線程
1.進程
是操作系統(tǒng)為應用程序分配資源的基本單位,比如操作系統(tǒng)會為一個應用程序分配獨立的工作空間,硬件資源,任務調度等。一個應用程序就是一個進程。
2.線程
是cpu執(zhí)行的基本單位,可以理解為一個基本的執(zhí)行流,一個進程中至少有一個線程。進程是線程的集合體或者載體。
3.線程模型
線程分為內核線程和用戶線程。
內核線程就是操作系統(tǒng)自己實現(xiàn)的一套線程機制,實現(xiàn)一套線程機制并不是像java一樣創(chuàng)建一個Thread對象即可,而是除了創(chuàng)建線程對象,還要基于操作系統(tǒng)及硬件的不同(比如cpu是多核單核等),制定何時凍結或喚醒線程、該給線程分配多少處理器執(zhí)行時間、該把線程安排給哪個處理器核心去執(zhí)行等事宜,而內核實現(xiàn)了這一套機制。
用戶線程就是用戶程序中創(chuàng)建的線程,和內核線程一樣,線程是交給處理器執(zhí)行,但是具體的執(zhí)行和分配機制需要用戶程序自己實現(xiàn),就是說也要根據(jù)機器的不同實現(xiàn)一套線程運行機制才行。但是實現(xiàn)這樣一套機制是極其復雜的,而且還要考慮不同的機器平臺,所以說有一定的困難度。
那我們開發(fā)的應用程序線程是怎么實現(xiàn)的呢,一般的應用程序實現(xiàn)的線程根據(jù)其特點可以分為三種:
- 用戶線程和內核一一對應的1:1模型
- 用戶線程和進程多對一的1:N模型
- 用戶線程和內核線程多對多的M:N模型
(1) 1:1模型
應用程序創(chuàng)建一個線程實際上是通過內核提供的內核線程api調用與一個內核線程建立一比一映射關系,也就是說看起來應用程序創(chuàng)建了用戶線程,但其實應用程序只是創(chuàng)建線程對象,通過線程對象調用了內核api創(chuàng)建了內核線程,對于應用程序來說就比較省事了,因為具體的線程機制都是由內核完成。
這種模型免不了用戶態(tài)到內核態(tài)的切換,性能方面會被限制。
(2) 1:N模型
就是上面所說的,由用戶程序自己實現(xiàn)線程機制,包括線程創(chuàng)建,線程調度等。
這種模型基本能避免用戶態(tài)到內核態(tài)的切換,這就可以支持更多的線程并發(fā),不好的地方就是實現(xiàn)困難。
(3) M:N模型
就是用戶程序中的用戶線程和內核線程不是一比一映射,而是多條用戶線程對應一條內核線程或者多條內核線程。
這種模型既能夠保證并發(fā)量,提高性能,又能利用內核的調度機制,但是免不了自己實現(xiàn)一套線程機制,依然具備很高的復雜性。
java在實現(xiàn)線程機制這條路上可以說對于三種模型都曾有過實現(xiàn)。其實采用哪種線程模型依賴于所使用的操作系統(tǒng)是否支持,就拿Hotspot這款虛擬機來說,它在不同操作系統(tǒng)上的實現(xiàn)就不一樣,在Solaris平臺的HotSpot虛擬機,由于操作系統(tǒng)的線程特性本來就可以同時支持 1:1及N:M
但是主流的java虛擬機hotspot在主流的操作系統(tǒng)win和linux,采用的都是1:1模型。即線程調度交個內核來完成,這樣我們在創(chuàng)建線程的時候就受到了資源的很大限制,比如我們無法做到創(chuàng)建大批量的線程出來。
舉個例子:
內核是一個國有工廠負責加工產品,cpu是加工產品的機器,一個用戶程序就是一個私有企業(yè),也就是一個進程,而線程就是私企和國企里面的工作人員。每個私企員工都會將一定的原材料送到國企進行加工。
1:1模型中,每當一個私企員工攜帶原材料來加工,都會有一個國企的員工接待,負責加工流程中的所有事情,直到加工完成并將私企員工送走。因為有很多私企,一個私企里面有很多私企員工,而國企員工相對較少,這樣一對一的服務,如果私企的人來的太多,會把國企人員累死,整個生產線會癱瘓。
1:N模型中就是每個私企在國企里面租一臺機器的使用時間,就是某個時間段,由某個私企承包,這個時間段內機器只為這個私企工作,這種情況下只要在這個時間段內,私企想派多少人去加工就派多少人去加工。
M:N模型中,某個私企大概有100人攜帶原材料加工,私企感覺這樣效率有點低,就想了個辦法,和國企達成某種協(xié)議,讓國企出幾個人專門服務于私企的這100個人,這樣效率會提升了。
4.線程調度
多線程情況下,能夠讓每個線程都能有條不紊的得到運行就是線程調度。
我們知道cpu處理器是單線流水運行,線程是運行在cpu上的,一個時間點上只會有一條線程正在被cpu運行。
基于以上情況,線程要如何調度呢,一般情況下,操作系統(tǒng)有兩種調度方式:
(1) 協(xié)同式調度
正在執(zhí)行的線程自己控制自身的執(zhí)行時間,并且當前線程執(zhí)行完后由自身告知操作系統(tǒng)可以調度到其他線程上了。
這種方式相對來說實現(xiàn)簡單,不用考慮同步的問題。
但是缺點也很明顯,這種調度方式中有兩個點要注意,線程多長時間可以執(zhí)行完?線程一定能通知操作系統(tǒng)調度其他線程嗎?這兩點無法保證的話就會引發(fā)一些問題,比如當前線程本身運行時間很長,執(zhí)行邏輯中有一些io等待操作,這就造成cpu是空閑的,資源大大浪費;再比如如果代碼或者業(yè)務邏輯出了問題稍有不慎就會造成無法通知操作系統(tǒng)調度其他線程,這個是災難性的。
(2) 搶占式調度
由操作系統(tǒng)為每個線程分配操作時間,并且由操作系統(tǒng)自主負責調度其他線程。
cpu處理整個過程被分成若干個極小的時間片段,每個線程可以被分配多個時間片段。然后操作系統(tǒng)會按照分配情況進行調度。
這種方式可以讓所有的線程看起來是同時運行的,即便有一條線程出了問題,也不會影響其他線程。
主流的平臺linux的內核線程都是搶占式調度,這種方式的缺點就是接下來要說的上下文切換。
4.線程上下文切換
當處理器要運行線程時,除了運行代碼外還要有上下文數(shù)據(jù)的支撐。而這里說的“上下文”,以程序員的角度來看,是方法調用過程中的各種局部的變量與資源;以線程的角度來看,是方法的調用棧中存儲的各類信息;而以操作系統(tǒng)和硬件的角度來看,則是存儲在內存、緩存和寄存器中的一個個具體數(shù)值。
物理硬件的各種存儲設備和寄存器是被操作系統(tǒng)內所有線程共享的資源,比如線程A正在運行,線程B正在掛起等待運行,此時線程A的時間片段運行完畢了,線程B就會被調度。從線程A切換到線程B去執(zhí)行之前,操作系統(tǒng)首先要把線程A的上下文數(shù)據(jù)妥善保管好,然后把寄存器、內存分頁等恢復到線程B掛起時候的狀態(tài),這樣線程B被重新激活后才能仿佛從來沒有被掛起過。這種保護和恢復現(xiàn)場的工作,免不了涉及一系列數(shù)據(jù)在各種寄存器、緩存中的來回拷貝,這便是上下文切換,同時這個操作不是一個輕量級的操作,你想每個時間片段是很短的,也就意味著短時間內要做大量的上下文切換,這就一定會造成很大的時間消耗。
說到這里,java開發(fā)中的線程是java線程和內核線程一比一映射的1:1線程模型,一般java運行在linux系統(tǒng),所以java開發(fā)中常遇到的性能影響點是”用戶態(tài)轉換內核態(tài)和上下文切換“。
5.java線程
java中的線程類是Thread,它定義了java層面線程創(chuàng)建,線程操作的一些列方法。
java中線程實現(xiàn)有三種方式:
//1.繼承thread
Student extends Thread
Student xiaoming = new Student("小明",punishment);
xiaoming.start();
//2.實現(xiàn)Runnable
Student implements Runnable
Thread xiaoming = new Thread(new Student("小明",punishment),"小明");
xiaoming.start();
//3.任務FutureTask實現(xiàn)
public static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception{
return "hello";
}
}
public static void main(String[] args) {
//創(chuàng)建異步任務
FutureTask<String> futureTask = new FutureTask<>(new CallerTask ());
new Thread(futureTask).start();
try{
String result=futureTask.get();
}catch(){
e.printStackTrace();
}
}
- 第一種方式繼承Thread類,所以子類也是一個Thread類,這樣子類是一個攜帶任務的線程類,任務和線程牢牢綁在一起不可分割,且子類繼承Thread類,就再也不能繼承其他類。
- 第二種方式,Runnable是任務類,子類實現(xiàn)Runnable后依然是一個任務類,而且還能實現(xiàn)其他的類,這種方式中任務和線程隔離,一個任務可以被多個線程執(zhí)行。符合java職責分離的設計原則和面向接口編程的原則。
以上兩種方式均沒有返回值,而第三種方式可以支持返回值,后面我們細講FutureTask。
我們來解析一下上面的前兩種方式:
Thread是java中的線程實體類,Runable是任務實體類,他們兩個的關系為,Runable代表任務,Thread代表任務的載體,以上面的例子來說,Thread就是企業(yè)內的工作人員,而Runable是具體的加工任務。
public interface Runnable {
public abstract void run();
}
Runnable是一個接口,run是任務方法,自定一個類并實現(xiàn)Runnable類,然后重寫run方法定義具體的任務邏輯,這個類就是一個具體的任務類。
任務類自身不能運行,需要依賴載體。
public class Thread implements Runnable {
private Runnable target;
public Thread() {
}
public Thread(Runnable target) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
target.run();
}
}
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
}
Thread線程作為任務的載體,通過實現(xiàn)Runnable而具備攜帶任務的能力,通過源碼可知,這個任務邏輯可以來自于自身,也可以來自外界傳進來的任務實體Runnable。
Thread線程除了可以攜帶任務,更重要的是能夠驅動任務運行,start方法就是啟動開關,其內部是本地方法,由jvm內部實現(xiàn),不難想象這個方法的底層一定是調用了內核線程api,與內核線程做映射,然后jvm回調java中的run方法。
6.java線程狀態(tài)
java中的線程具有六種狀態(tài)。
- 新建(New):創(chuàng)建后尚未啟動的線程處于這種狀態(tài)。
- 運行(Runnable):包括操作系統(tǒng)線程狀態(tài)中的Running和Ready,也就是處于此狀態(tài)的線程有可能正在執(zhí)行,也有可能正在等待著操作系統(tǒng)為它分配執(zhí)行時間。
- 等待(Waiting):處于這種狀態(tài)的線程不會被分配處理器執(zhí)行時間,它們要等待被其他線喚醒。一般在調用wait方法和sleep方法的時候處于這種狀態(tài)。
- 阻塞(Blocked):線程被阻塞了,“阻塞狀態(tài)”與“等待狀態(tài)”的區(qū)別是“阻塞狀態(tài)”在等待著獲取到一個排它鎖,這個事件將在另外一個線程放棄這個鎖的時候發(fā)生;而“等待狀態(tài)”則是在等待一段時間,或者喚醒動作的發(fā)生。在程序等待進入同步區(qū)域的時候,線程將進入這種狀態(tài)。
- 結束(Terminated):已終止線程的線程狀態(tài),線程已經結束執(zhí)行。
7.java 線程 API
睡眠
public static native void sleep(long millis);
獲取當前線程對象
public static native Thread currentThread();
讓出cpu
public static native void yield();
設置優(yōu)先級
public final void setPriority(int newPriority);
阻塞等待執(zhí)行完成
public final void join();
設置為守護線程
public final void setDaemon(boolean on);
讓可中斷方法中斷
public void interrupt();
獲取中斷位狀態(tài)
public static boolean interrupted();
獲取中斷位狀態(tài),并重置
public boolean isInterrupted();
這些方法有一些不是很常用,所以了解即可,重點說一下join,interrupt,interrupted,isInterrupted。
join方法是一個成員方法,調用某個線程的join方法,會讓主線程一直處于wait狀態(tài)(join方法底層源碼中通過不斷調用wait方法實現(xiàn)),等待這個線程處理完成再繼續(xù)執(zhí)行下去。
interrupt是一個成員方法,這個方法會中斷靜止線程,而運行中的線程只能被設置為可中斷標志位為true,怎么理解呢?就是說如果線程處于sleep、wait狀態(tài),這個時候調用interrupt會把這個阻塞狀態(tài)喚醒并拋出異常,之后該線程繼續(xù)運行,如果線程正在運行中(非Waiting狀態(tài)),此時調用interrupt只會給線程的可中斷狀態(tài)設置為true,對線程運行不會有任何影響。
調用某個線程的interrupt方法,給線程設置可中斷狀態(tài)的意義是什么呢?因為線程運行過程中可能正在處理數(shù)據(jù),這個時候如果人為中斷線程是不安全的,所以要等到線程處于一個安全位置中斷才比較合理,那什么時候才是安全的位置呢,這個程序員自己決定,當處于安全位置的時候給一個可中斷標示,這樣中斷的時候判斷一下可中斷狀態(tài)再決定是否中斷即可。juc源碼中隨處可見該方法的應用。
上面說了中斷的時候判斷一下可中斷的狀態(tài),interrupted這個方法就是獲取線程的中斷狀態(tài),而isInterrupted方法也是獲取線程的可中斷狀態(tài),不同點在于isInterrupted方法在獲取中斷狀態(tài)后,會順便把中斷狀態(tài)重置為false。