目錄
-
- 使用者線程和核心線程
- Java使用的線程
- JVM中線程的建立
-
- new java.lang.Thread().start()
- 使用JNI将一個native thread attach到JVM中
- Java中實作線程的方式
- 線程的生命周期
- 線程上下文切換及死鎖
-
- 線程上下文切換
- 死鎖
什麼是線程?
現代作業系統在運作一個程式時,會為其建立一個程序。例如,啟動一個Java程式,作業系統就會建立一個Java程序。現代作業系統排程CPU的最小單元是線程,也叫輕量級程序(Light Weight Process),在一個程序裡可以建立多個線程,這些線程都擁有各自的計數器、堆棧和局部變量等屬性,并且能夠通路共享的記憶體變量。處理器在這些線程上高速切換, 讓使用者感覺到這些線程在同時執行。
使用者線程和核心線程
使用者線程
指不需要核心支援而在使用者程式中實作的線程,其不依賴于作業系統核心,應
用程序利用線程庫提供建立、同步、排程和管理線程的函數來控制使用者線程。另外,使用者線程是由應用程序利用線程庫建立和管理,不依賴于作業系統核心。不需要使用者态/核心态切換,速度快。作業系統核心不知道多線程的存在,是以一個線程阻塞将使得整個程序(包括它的所有線程)阻塞。由于這裡的處理器時間片配置設定是以程序為基本機關,是以每個線程執行的時間相對減少。

核心線程
線程的所有管理操作都是由作業系統核心完成的。核心儲存線程的狀态和上下
文資訊,當一個線程執行了引起阻塞的系統調用時,核心可以排程該程序的其他線程執行。在多處理器系統上,核心可以分派屬于同一程序的多個線程在多個處理器上運作,提高程序執行的并行度。由于需要核心完成線程的建立、排程和管理,是以和使用者級線程相比這些操作要慢得多,但是仍然比程序的建立和管理操作要快。大多數市場上的作業系統,如Windows, Linux等都支援核心級線程。
Java使用的線程
java在1.2之前使用的是使用的使用者線程,後來改變使用核心線程,這也是為什麼Thread類中的start0()為native方法,就是去調用作業系統庫中建立核心線程。
JVM中線程的建立
在jvm中建立線程有兩種方式(注意:這裡說建立一個線程是指核心線程,并不是我們常說的繼承Thread或者實作Runnable/Callable接口,繼承Thread或者實作Runbale/Callable隻是java用于指定核心線程要執行的應用程式中要執行的代碼。)
new java.lang.Thread().start()
在Thread的start()方法中,調用了其start0()方法,而該方法是一個native的方法,通過調用該方法産生一個核心線程然後再執行Thread的run方法。
其流程大緻如下:
- 建立對應的JavaThread的instance
- 建立對應的OSThread的instance
- 建立實際的底層作業系統的native thread
- 準備相應的JVM狀态,比如ThreadLocal存儲空間配置設定等
- 底層的native thread開始運作,調用java.lang.Thread生成的Object 的run()方法
- 當java.lang.Thread生成的Object的run()方法執行完畢傳回後,或者抛出 異常終止後,終止native thread
- 釋放JVM相關的thread的資源,清除對應的JavaThread和OSThread
使用JNI将一個native thread attach到JVM中
其大緻流程如下:
- 通過JNI call AttachCurrentThread申請連接配接到執行的JVM執行個體
- JVM建立相應的JavaThread和OSThread對象
- 建立相應的java.lang.Thread的對象
- 一旦java.lang.Thread的Object建立之後,JNI就可以調用Java代碼了
- 當通過JNI call DetachCurrentThread之後,JNI就從JVM執行個體中斷開連接配接
- JVM清除相應的JavaThread, OSThread, java.lang.Thread對象
Java中實作線程的方式
通過2.3接我們知道,在代碼中通過Thread的start0()方法建立一個核心線程,這個線程在相應的資源準備好之後會回調jvm中Thread執行個體的run()方法,可以說是底層給應用程式開放的一個接口供開發者調用進而實作多線程。
那麼Java中有幾種實作線程的方法呢?
- 繼承Thread
public class MyThread extends Thread{ @Override public void run() { System.out.println("i am a thread, i am running"); } public static void main(String[] args) { Thread thread = new MyThread(); // 開啟線程 thread.start(); } }
- 通過實作Runable接口
通過上面代碼我們可以看到,通過實作Runnable接口仍然需要借助Thread類的執行個體運作start()方法啟動線程,因為隻有Thread類中存在調用底層庫建立核心線程的本地方法start0().public class MyThreadImpl implements Runnable { public void run() { System.out.println("i am a thread, i am running"); } public static void main(String[] args) { new Thread(new MyThreadImpl()).start(); } }
- 通過實作Callable(有傳回結果)接口
public class MyThreadCallImpl implements Callable<String> { public String call() throws Exception { System.out.println("i am thread, i can return result, i am running"); return "result"; } public static void main(String[] args) throws Exception { FutureTask<String> result = new FutureTask<String>(new MyThreadCallImpl()); // 開啟線程 new Thread(result).start(); // 主線程阻塞一直等待結果傳回 String resMsg = result.get(); } }
通過實作Callable的方式啟動線程必須将其實作通過參數傳入FutureTask類的執行個體中,然後再經過建立一個Thread執行個體将FutureTask作為參數傳入并啟動來開啟線程,那麼這個FutureTask究竟是什麼?為什麼實作Callale開啟線程和擷取結果都需要經過他呢?
下圖是FutureTask的繼承路徑那麼他是怎麼實作有傳回結果的呢?其大緻流程如下:
并發多線程之線程 - 通過Thread的start()方法啟動線程,那麼線程就會回調run()方法,而從圖中我們可以看到FutureTask是Runnable的實作類,是以會調用FutureTaske類的run()方法;
- 在FutureTask的run()方法中,調用參數Callable的實作類的執行個體的call()方法,并存儲在FutureTask的執行個體中;
- 通過FutureTask的get()方法擷取Callable執行個體的call方法傳回的結果。
線程的生命周期
線程的生命周期流程如下:
接下來着重說幾個方法
- wait() 該方法會讓出cpu,同時也會釋放鎖;
- sleep() 讓線程休眠一段時間,等到預計時間後再恢複執行,線程休眠會讓線 程讓出cpu的執行權,但不會釋放鎖,sleep會抛出 InterruptedException,還需要說明的是sleep睡眠的時間不一定就是參數指定的時間,因為在參數指定的時間到達後線程還需要争搶cpu的執行權,這也是需要耗費時間的;
-
join() join會讓線程讓出cpu執行權,同時會釋放鎖,join就是對wait的一
個封裝,我們可以檢視Thread類的join(long)方法,有一段關鍵代碼如下:
其意思就是目前線程存活,那麼在被join的線程就一直wait直到join的線程執行完畢,join會抛出會抛出 InterruptedException異常;while (isAlive()) { wait(0); }
- yield() 線程讓步暫停執行目前的線程對象,并執行其他線程,yield會讓目前 線程讓出cpu,而不會釋放鎖。yield()方法無法控制具體交出CPU的時間,并且yield()方法隻能讓擁有相同優先級的線程有擷取CPU的機會;
- interrupt() 、isInterrupted()和 interrupted()
1、interrupt()方法隻是将線程狀态置為中斷狀态而已(中斷狀态為這是為true),它不會中斷一個正在運作的線程,此方法隻是給線程傳遞一個中斷信号,程式可以根據此信号來判斷是否需要終止。
2、當線程中使用wait()、sleep()、join()導緻此線程阻塞,則interrupt()會線上程中抛出InterruptException,并且将線程的中斷狀态由true置為false。
3、isInterrupted()擷取目前線程的中斷辨別
4、interrupted() 擷取目前線程的中斷辨別,并會将目前線程的中斷辨別設定為false,這是與isInterrupted()的差別
接下來看幾個demo。
demo1:
demo2:public class ThreadDemo extends Thread{ public static void main(String[] args) { ThreadDemo demo = new ThreadDemo(); demo.start(); } /** * 運作結果 * true * true * false */ public void run() { // 1、設定線程中斷狀态會true Thread.currentThread().interrupt(); // 2、擷取目前線程的中斷狀态,第1步已經設定為true了,是以此處是true System.out.println(this.isInterrupted()); // 3、因為在第一步中斷狀态為true,是以此處為true,當時interrupted()會清除中斷狀态, System.out.println(Thread.interrupted()); // 因為在第3步調用Thread.interrupted()是以此時中斷轉狀态又變成了最開始的false System.out.println(Thread.interrupted()); } }
通過上面的例子我們應該可以更加深刻的了解Thread中的interrupt() 、isInterrupted()和 interrupted()三個方法的關系。public class ThreadDemo2 extends Thread{ public static void main(String[] args) { ThreadDemo2 demo2 = new ThreadDemo2(); demo2.start(); } /** * 運作結果 * true * 異常資訊... * false */ @Override public void run() { // 将中斷終态設定為true this.interrupt(); // 檢視目前線程的中斷狀态(隻是檢視,不修改) System.out.println(this.isInterrupted()); try{ Thread.sleep(100); } catch (Exception ex) { ex.printStackTrace(); // sleep抛出異常并将中斷狀态修改為false System.out.println(this.isInterrupted()); } } }
線程上下文切換及死鎖
在開始之前我們先要了解為什麼要用到并發:
并發程式設計的本質其實就是利用多線程技術,在現代多核的CPU的背景下,催生了并發程式設計的趨勢,通過并發程式設計的形式可以将多核CPU的計算能力發揮到極緻,性能得到提升。除此之外,面對複雜業務模型,并行程式會比串行程式更适應業務需求,而并發程式設計更能吻合這種業務拆分 。即使是單核處理器也支援多線程執行代碼,CPU通過給每個線程配置設定CPU時間片來實作這個機制。時間片是CPU配置設定給各個線程的時間,因為時間片非常短,是以CPU通過不停地切 換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。 并發不等于并行:并發指的是多個任務交替進行,而并行則是指真正意義上的“同時進行”。實際上,如果系統内隻有一個CPU,而使用多線程時,那麼真實系統環境下不能并行, 隻能通過切換時間片的方式交替進行,而成為并發執行任務。真正的并行也隻能出現在擁有多個CPU的系統中。
并發的優點:
- 充分利用多核CPU的計算能力;
- 友善進行業務拆分,提升應用性能;
當然有利就就有弊,引入并發同時也帶來了響應的問題:
- 高并發下導緻頻繁的上下文切換;
- 臨界區下産生安全問題,容易産生死鎖導緻系統不可用;
- 其他。
接下來我們對第1和2這個問題說明一下。
線程上下文切換
上面我們已經了解到線程是通過向cpu申請時間片進而進而獲得cpu的執行權,下面我們假設有這麼一種情況(不代表所有情況),線程A和線程B,如下圖:
這裡線程A擷取cpu配置設定的時間片開始執行,當執行一段時間(還未執行完),這時線程B擷取到cpu配置設定的時間片, 為了保證線程A再次執行時可以從正确的位置,并且狀态正确的執行,需要将線程A的目前狀态儲存,那麼儲存到哪裡呢?儲存到核心空間的一個叫Tss任務段的記憶體空間(操作核心空間當然是需要最高權限Ring0,這就是為什麼線程上下文切換涉及到由使用者态切換到核心态的原因),将線程A運作資料由寄存器儲存到Tss任務端,然後加載線程A的指令等資訊到寄存器執行線程B,當線程B執行完畢,這時線程A擷取到cpu配置設定的時間片,那麼需要從Tss任務段将直線儲存的資訊加載到寄存器繼續執行線程A(這裡又涉及到由使用者态切換到核心态)。
從上面的流程我們大緻了解了線程上下文切換的流程,那麼為什麼線程上下文的頻繁切換會影響性能了吧,頻繁的加載和儲存資料也是會耗費cpu執行正常流程的時間的。
死鎖
多線程還會導緻死鎖的發生,這裡死鎖是什麼就不解釋了,主要說一下最常見的死鎖場景,如圖:
- 線程執行擷取鎖1;
- 線程B執行擷取鎖2;
- 線程A繼續執行,要擷取鎖2,此時線程B持有鎖2,那麼線程A阻塞直到擷取鎖2;
- 線程B繼續執行,要擷取鎖1,此時線程A持有鎖1,那麼線程B阻塞直到擷取鎖1;
- 兩個線程都阻塞不會繼續執行,那麼兩個線程會一直不釋放持有的鎖,那麼會一直阻塞下去,産生死鎖,新進來的線程如果要擷取鎖1或者鎖2都會阻塞進而導緻系統不可用。
看一以下代碼:
public class DealLockTest {
public static final String LOCK_A = "A";
public static final String LOCK_B = "B";
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
public void run() {
synchronized (LOCK_A) {
try {
Thread.sleep(200);
System.out.println("擷取A鎖");
} catch (Exception ex) {
}
synchronized (LOCK_B) {
System.out.println("擷取B鎖");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
synchronized (LOCK_B) {
System.out.println("擷取B鎖");
synchronized (LOCK_A) {
System.out.println("擷取B鎖");
}
}
}
});
t1.start();
t2.start();
}
}
上面例子就是對死鎖的複現,一般我們可以通過java提供的工具jps和jstack進行問題的定位,使用jps查找到啟動的程式程序:
然後使用jstack檢視資訊 jstack 8656,死鎖資訊如下: