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