Java中的多執行緒分析
一、 引言
1、程序
通俗的來說,程序就是我們開啟電腦後使用的一個個的應用程式。比如:開啟QQ會啟動一個程序,開啟瀏覽器又會啟動另一個程序。程序和我們的程式一一對應。關於程序特點,如下:
(1)、 每個程序對應一定的記憶體地址空間,並且只能使用它自己的記憶體空間,各個程序間互不干擾。 並且,程序儲存了程式每個時刻的執行狀態,這樣就為程序切換提供了可能; (2)、 一個程序可以包含多個執行緒; (3)、 程序讓作業系統的併發性成為可能,但是要注意,一個程序雖然包括多個執行緒,但是這些執行緒是共同享有程序佔有的資源 和地址空間的。 (4)、 程序是作業系統進行資源分配的基本單位,而執行緒是作業系統進行排程的基本單位。
2、執行緒
在出現了程序以後,作業系統的效能得到了大大的提升。雖然程序的出現解決了作業系統的併發問題,但是人們仍然不滿足,人們逐漸對 實時性 有了要求。因為一個程序在一個時間段內只能做一件事情,如果一個程序有多個子任務,只能逐個地去執行這些子任務。所以,執行緒就應運而生,特點如下:
(1)、 執行緒讓程序的內部併發成為可能;
(2)、 執行緒允許在同一個程序中同時存在多個程式控制流。
執行緒會共享程序範圍內的資源,但每個執行緒都有各自的 程式計數器 、棧 以及 區域性變數 等;
在 Java 中,一個應用程式對應著一個JVM例項(JVM程序)。Java採用的是 單執行緒程式設計模型 ,即在我們自己的程式中如果沒有主動建立執行緒的話,只會建立一個執行緒,通常稱為主執行緒。但是要注意,雖然只有一個執行緒來執行任務,不代表JVM中只有一個執行緒,JVM例項在建立的時候,同時會建立很多其他的執行緒(比如垃圾收集器執行緒)。
3、使用執行緒,會帶來哪些問題?
(1)、安全性問題
線上程安全性的定義中,最核心的概念就是正確性。當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將
如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。
(2)、活躍性問題
活躍性問題關注的是:某件正確的事情最終會發生。導致活躍性的問題包括死鎖、飢餓等。
(3)、效能問題
效能問題關注的是:正確的事情能夠儘快發生。效能問題包括多個方面,例如響應不靈敏,吞吐率過低,資源消耗過高等。 在多執行緒程式中,當執行緒排程器臨時掛起活躍執行緒並轉而執行另一個執行緒時,就會頻繁出現上下文切換操作(Context Switch), 這種操作會導致 CPU 時間更多的花線上程排程上而非執行緒的執行上。
4、執行緒的排程策略
如果處於就緒狀態的執行緒獲得了CPU,就會開始執行run()方法的執行緒任務,則該執行緒就會處於執行狀態。如果計算機只有一個CPU。那麼在任何時刻只有一個執行緒處於執行狀態,當然在一個多處理器的機器上,將會有多個執行緒並行執行;當執行緒數大於處理器數時,依然會存在多個執行緒在同一個CPU上輪換的現象。
當一個執行緒開始執行後,它不可能一直處於執行狀態(除非它的執行緒執行體足夠短,瞬間就執行結束了)。執行緒在執行過程中需要被中斷,目的是使其他執行緒獲得執行的機會,執行緒排程的細節取決於底層平臺所採用的策略。對於採用搶佔式策略的系統而言,系統會給每個可執行的執行緒一個小時間段來處理任務;當該時間段用完後,系統就會剝奪該執行緒所佔用的資源,讓其他執行緒獲得執行的機會。在選擇下一個執行緒時,系統會考慮執行緒的優先順序。
所有現代的桌面和伺服器作業系統都採用搶佔式排程策略,但一些小型裝置如手機則可能採用協作式排程策略,在這樣的系統中,只有當一個執行緒呼叫了它的sleep()或yield()方法後才會放棄所佔用的資源——也就是必須由該執行緒主動放棄所佔用的資源。
二、 執行緒的生命週期
Java 中 Thread類 的各種操作與執行緒的生命週期密不可分,所以瞭解清楚執行緒的生命週期,有助於更好的理解Thread類中的各個方法。
一般來說,執行緒從最初的建立到最終的消亡,要經歷建立(new)、就緒(runnable)、執行(running)、阻塞(blocked)、等待(waiting)、時間等待(time waiting) 和 消亡(dead/terminated)等狀態。在給定的時間點上,一個執行緒只能處於一種狀態。而線上程的生命週期中,各個狀態的切換,需要上下文的切換,即通過儲存和恢復CPU狀態使得其能夠從中斷點恢復執行。線上程生命週期中,呼叫各個方法時,要特別關注兩個問題:
(1)、客戶端呼叫某個方法後,是否會釋放鎖;
(2)、客戶端呼叫某個方法後,是否會交出CPU;
執行緒的各個狀態:
(1)、 新建狀態(new):當程式使用 new 關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態。此時僅由 JVM 為其分配記憶體,
並初始化其成員變數的值;
(2)、 就緒狀態(runnalbe):當執行緒物件呼叫了 start() 方法之後,該執行緒處於就緒狀態。JVM會為其建立方法呼叫棧和程式計數器,
等待排程執行;
(3)、 執行狀態(running):如果處於就緒狀態的執行緒獲得了CPU,開始執行run()方法的執行緒執行體,則該執行緒處於執行狀態;
(4)、 阻塞狀態(blocked):當處於執行狀態的執行緒失去所佔用資源之後,便進入阻塞狀態;
(5)、 消亡(dead):當執行緒執行完畢或被其它執行緒殺死,執行緒就進入死亡狀態,這時執行緒不可能再進入就緒狀態等待執行;
執行緒生命週期分析:
當我們需要執行緒來執行某個子任務時,就必須先建立一個執行緒。但是執行緒建立之後,並不會立即進入就緒狀態,因為執行緒的執行需要一些條件(比如程式計數器、Java棧、本地方法棧等),只有執行緒執行需要的所有條件滿足了,才進入就緒狀態。當執行緒進入就緒狀態後,不代表立刻就能獲取CPU執行時間,也許此時CPU正在執行其他的事情,因此它要等待。當得到CPU執行時間之後,執行緒便真正進入執行狀態。執行緒在執行狀態過程中,可能有多個原因導致當前執行緒不能繼續執行下去,比如使用者主動讓執行緒睡眠(睡眠一定的時間之後再重新執行)、使用者主動讓執行緒等待,或者被同步塊阻塞,此時就對應著多個狀態:time waiting(睡眠或等待一定的時間)、waiting(等待被喚醒)、blocked(阻塞)。當由於突然中斷或者子任務執行完畢,執行緒就會被消亡。
實際上,Java只定義了六種執行緒狀態,分別是 New、 Runnable、 Waiting、Timed Waiting、Blocked、Terminated。各狀態直接的轉換如下圖所示:
三、 何為上下文切換?
以單核CPU為例,CPU在一個時刻只能執行一個執行緒。CPU在執行一個執行緒的過程中,轉而去執行另外一個執行緒,這個叫做執行緒 上下文切換。
由於可能當前執行緒的任務並沒有執行完畢,所以在切換時需要儲存執行緒的執行狀態,以便下次重新切換回來時能夠緊接著之前的狀態繼續執行。
舉個簡單的例子:比如,一個執行緒A正在讀取一個檔案的內容,正讀到檔案的一半,此時需要暫停執行緒A,轉去執行執行緒B,當再次切換回來執行執行緒A的時候,我們不希望執行緒A又從檔案的開頭來讀取。
因此需要記錄執行緒A的執行狀態,那麼會記錄哪些資料呢?因為下次恢復時需要知道在這之前當前執行緒已經執行到哪條指令了,所以需要記錄程式計數器的值,另外比如說執行緒正在進行某個計算的時候被掛起了,那麼下次繼續執行的時候需要知道之前掛起時變數的值時多少,因此需要記錄CPU暫存器的狀態。所以,一般來說,執行緒上下文切換過程中會記錄程式計數器、CPU暫存器狀態等資料。
實質上, 執行緒的上下文切換就是儲存和恢復CPU狀態的過程,它使得執行緒執行能夠從中斷點恢復執行,這正是有程式計數器所支援的。
雖然多執行緒可以使得任務執行的效率得到提升,但是由於線上程切換時同樣會帶來一定的開銷代價,並且多個執行緒會導致系統資源佔用的增加,所以在進行多執行緒程式設計時要注意這些因素。
總結:上下文切換要做哪些事?
答:需要程式計數器來記錄該執行緒執行到哪條指令,需要暫存器來記錄該當前執行緒掛起時,當前變數的值,以便下次恢復時執行緒繼續執行。
四、 執行緒的建立
1、執行緒的建立方式
在 Java 中,建立執行緒一般有兩種方式:繼承 Thread 類和實現 Runnable 介面。其中,Thread 類本身就實現了 Runnable 介面,而使用繼承 Thread 類的方式建立執行緒的最大侷限就是不支援多繼承。特別需要注意兩點,
(1)、實現多執行緒必須重寫run()方法,即在run()方法中定義需要執行的子任務;
(2)、run()方法不需要使用者來呼叫;
執行緒建立的程式碼示例:
public class ThreadTest {
public static void main(String[] args) {
//使用繼承Thread類的方式建立執行緒
new Thread(){
@Override
public void run() {
System.out.println("Thread");
}
}.start();
//使用實現Runnable介面的方式建立執行緒
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Runnable");
}
});
thread.start();
//JVM 建立的主執行緒 main
System.out.println("main");
}
}
Output: (程式碼的執行結果與程式碼的執行順序或呼叫順序無關)
Thread
main
Runnable
建立好自己的執行緒類之後,就可以建立執行緒物件了,然後通過start()方法去啟動執行緒。注意,run() 方法中只是定義需要執行的任務,並且其不需要使用者來呼叫。當通過 start() 方法啟動一個執行緒之後,若執行緒獲得了CPU執行時間,便進入run()方法體去執行具體的任務。如果使用者直接呼叫run()方法,即相當於在主執行緒中執行run()方法,跟普通的方法呼叫沒有任何區別,此時並不會建立一個新的執行緒來執行定義的任務。
五、Thread 類詳解
Thread 類實現了 Runnable 介面,在 Thread 類中,有一些比較關鍵的屬性,比如name是表示Thread的名字,可以通過Thread類的構造器中的引數來指定執行緒名字,priority 表示執行緒的優先順序(最大值為10,最小值為1,預設值為5),daemon表示執行緒是否是守護執行緒,target 表示要執行的任務。
1、與執行緒執行狀態有關的方法
(1)、start() 方法
start()方法的作用:用來啟動一個執行緒,當呼叫該方法後,相應執行緒就會進入就緒狀態,該執行緒中的run()方法會在某個時機被呼叫。
(2)、run() 方法
run()方法是不需要使用者來呼叫的。當通過start()方法啟動一個執行緒之後,一旦執行緒獲得了 CPU 執行時間,便進入run()方法體去執行具體的任務。
注意
建立執行緒時必須重寫run()方法,以定義具體要執行的任務;一般來說,有兩種方式可以達到重寫run()方法的效果:
a.直接重寫:直接繼承Thread類並重寫run()方法;
b.間接重寫:通過Thread建構函式傳入Runnable物件,實際上重寫的是 Runnable物件 的run() 方法。
(3)、sleep() 方法
sleep() 方法的作用是:在指定的毫秒數內讓當前正在執行的執行緒睡眠(即 currentThread() 方法所返回的執行緒),並交出 CPU 讓其去執行其他的任務。當執行緒睡眠時間滿後,不一定會立即得到執行,因為此時 CPU 可能正在執行其他的任務。所以說,呼叫sleep方法相當於讓執行緒進入阻塞狀態(blocked)。該方法有如下特徵:
a. 使呼叫該方法的執行緒暫停執行一段時間,讓其他執行緒有機會繼續執行,但它並不釋放物件鎖。
也就是說如果有synchronized同步快,其他執行緒仍然不能訪問共享資料。注意該方法要捕捉異常。
b. sleep是幫助其他執行緒獲得執行機會的最好方法,但是如果當前執行緒獲取到的有鎖,sleep不會讓出鎖。
c. 執行緒睡眠到期自動甦醒,並返回到可執行狀態(就緒),不是執行狀態。
d. 睡眠執行緒在甦醒之後,並不會立即執行,所以sleep()中指定的時間是執行緒不會執行的最短時間,
sleep方法不能作為精確的時間控制。
(4)、yield() 方法
yield()方法的作用:會讓當前執行緒交出CPU資源,讓CPU去執行其他的執行緒。但是,yield()不能控制具體的交出CPU的時間。需要注意的是:
a. yield()方法只能讓擁有相同優先順序的執行緒有獲取 CPU 執行時間的機會;
b. 呼叫yield()方法並不會讓執行緒進入阻塞狀態,而是讓執行緒重回就緒狀態,它只需要等待重新得到 CPU 的執行;
c. 它同樣不會釋放鎖。
public class MyThread extends Thread {
@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000; i++) {
Thread.yield(); // 將該語句註釋後,執行會變快
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用時:" + (endTime - beginTime) + "毫秒!");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
(5)、 join() 方法
假如在main執行緒中呼叫thread.join()方法,則main執行緒會等待thread執行緒執行完畢或者等待一定的時間。詳細地,如果呼叫的是無參join方法,則等待thread執行完畢;如果呼叫的是指定了時間引數的join方法,則等待一定的時間。join()方法有三個過載方法:
public final void join() throws InterruptedException {...}
public final synchronized void join(long millis) throws InterruptedException {...}
public final synchronized void join(long millis, int nanos) throws InterruptedException {...}
以 join(long millis) 方法為例,其內部呼叫了Object的wait()方法,如下圖:
根據以上原始碼可以看出,join()方法是通過wait()方法 (Object 提供的方法) 實現的。當 millis == 0 時,會進入 while(isAlive()) 迴圈,並且只要子執行緒是活的,宿主執行緒就不停的等待。 wait(0) 的作用是讓當前執行緒(宿主執行緒)等待,而這裡的當前執行緒是指 Thread.currentThread() 所返回的執行緒。所以,雖然是子執行緒物件(鎖)呼叫wait()方法,但是阻塞的是宿主執行緒。
看下面的例子,當 main執行緒 執行到 thread1.join() 時,main執行緒會獲得執行緒物件thread1的鎖(wait 意味著拿到該物件的鎖)。只要 thread1執行緒 存活, 就會呼叫該物件鎖的wait()方法阻塞 main執行緒,直至 thread1執行緒 退出才會使 main執行緒 得以繼續執行。
//示例程式碼
public class Test {
public static void main(String[] args) throws IOException {
System.out.println("進入執行緒"+Thread.currentThread().getName());
Test test = new Test();
MyThread thread1 = test.new MyThread();
thread1.start();
try {
System.out.println("執行緒"+Thread.currentThread().getName()+"等待");
thread1.join();
System.out.println("執行緒"+Thread.currentThread().getName()+"繼續執行");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("進入執行緒"+Thread.currentThread().getName());
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
// TODO: handle exception
}
System.out.println("執行緒"+Thread.currentThread().getName()+"執行完畢");
}
}
}
/* Output:
進入執行緒main
執行緒main等待
進入執行緒Thread-0
執行緒Thread-0執行完畢
執行緒main繼續執行
*///~
看上面的例子,當 main執行緒 執行到 thread1.join() 時,main執行緒會獲得執行緒物件thread1的鎖(wait 意味著拿到該物件的鎖)。只要 thread1執行緒 存活, 就會呼叫該物件鎖的wait()方法阻塞 main執行緒。那麼,main執行緒被什麼時候喚醒呢?事實上,有wait就必然有notify。在整個jdk裡面,我們都不會找到對thread1執行緒的notify操作。這就要看jvm程式碼了:
//一個c++函式:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) ;
//這個函式的作用就是在一個執行緒執行完畢之後,jvm會做的收尾工作。裡面有一行程式碼:ensure_join(this);
該函式原始碼如下:
static void ensure_join(JavaThread* thread) {
Handle threadObj(thread, thread->threadObj());
ObjectLocker lock(threadObj, thread);
thread->clear_pending_exception();
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
java_lang_Thread::set_thread(threadObj(), NULL);
//thread就是當前執行緒,就是剛才說的thread1執行緒。
lock.notify_all(thread);
thread->clear_pending_exception();
}
至此,thread1執行緒物件鎖呼叫了notifyall,那麼main執行緒也就能繼續跑下去了。
由於 join方法 會呼叫 wait方法 讓宿主執行緒進入阻塞狀態,並且會釋放執行緒佔有的鎖,並交出CPU執行許可權。結合 join 方法的宣告,有以下三條:
a. join方法同樣會會讓執行緒交出CPU執行許可權;
b. join方法同樣會讓執行緒釋放對一個物件持有的鎖;
c. 如果呼叫了join方法,必須捕獲InterruptedException異常或者將該異常向上層丟擲。
(6)、interrupt() 方法
interrupt()的作用:單獨呼叫interrupt()方法可以使得 處於阻塞狀態的執行緒 丟擲一個異常,也就是說,它可以用來中斷一個正處於阻塞狀態的執行緒;另外,通過 interrupted()方法 和 isInterrupted()方法 可以停止正在執行的執行緒。interrupt 方法在 JDK 中的定義為:
interrupted() 和 isInterrupted()方法在 JDK 中的定義分別為:
下面看一個例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
try {
System.out.println("進入睡眠狀態");
Thread.currentThread().sleep(10000);
System.out.println("睡眠完畢");
} catch (InterruptedException e) {
System.out.println("得到中斷異常");
}
System.out.println("run方法執行完畢");
}
}
}
/* Output:
進入睡眠狀態
得到中斷異常
run方法執行完畢
*///~
從這裡可以看出,通過interrupt()方法可以中斷處於阻塞狀態的執行緒。那麼能不能中斷處於非阻塞狀態的執行緒呢?看下面這個例子:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(i<Integer.MAX_VALUE){
System.out.println(i+" while迴圈");
i++;
}
}
}
}
執行該程式會發現,while迴圈會一直執行直到變數i的值超出Integer.MAX_VALUE。所以說,直接呼叫interrupt() 方法不能中斷正在執行中的執行緒。但是,如果配合 isInterrupted()/interrupted() 能夠中斷正在執行的執行緒,因為呼叫interrupt()方法相當於將中斷標誌位置為true,那麼可以通過呼叫isInterrupted()/interrupted()判斷中斷標誌是否被置位來中斷執行緒的執行。比如下面這段程式碼:
public class Test {
public static void main(String[] args) throws IOException {
Test test = new Test();
MyThread thread = test.new MyThread();
thread.start();
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
}
thread.interrupt();
}
class MyThread extends Thread{
@Override
public void run() {
int i = 0;
while(!isInterrupted() && i<Integer.MAX_VALUE){
System.out.println(i+" while迴圈");
i++;
}
}
}
}
但是,一般情況下,不建議通過這種方式來中斷執行緒,一般會在MyThread類中增加一個 volatile 屬性 isStop 來標誌是否結束 while 迴圈,然後再在 while 迴圈中判斷 isStop 的值。例如:
class MyThread extends Thread{
private volatile boolean isStop = false;
@Override
public void run() {
int i = 0;
while(!isStop){
i++;
}
}
public void setStop(boolean stop){
this.isStop = stop;
}
}
那麼,就可以在外面通過呼叫setStop方法來終止while迴圈。
(7)、stop()方法
stop() 方法已經是一個 廢棄的 方法,它是一個 不安全的 方法。因為呼叫 stop() 方法會直接終止run方法的呼叫,並且會丟擲一個ThreadDeath錯誤,如果執行緒持有某個物件鎖的話,會完全釋放鎖,導致物件狀態不一致。所以, stop() 方法基本是不會被用到的。
2、執行緒的暫停與恢復
(1)、執行緒的暫停、恢復方法在 JDK 中的定義
暫停執行緒意味著此執行緒還可以恢復執行。在 Java 中,可以使用 suspend() 方法暫停執行緒,使用 resume() 方法恢復執行緒的執行,但是這兩個方法已被廢棄,因為它們具有固有的死鎖傾向。如果目標執行緒掛起時在保護關鍵系統資源的監視器上保持有鎖,則在目標執行緒重新開始以前,任何執行緒都不能訪問該資源。如果重新開始目標執行緒的執行緒想在呼叫 resume 之前鎖定該監視器,則會發生死鎖。
例項方法 suspend() 在類Thread中的定義:
例項方法 resume() 在類Thread中的定義:
(2)、 死鎖
具體地,在使用 suspend() 和 resume() 方法時,如果使用不當,極易造成公共的同步物件的獨佔,使得其他執行緒無法得到公共同步物件鎖,從而造成死鎖。下面舉兩個示例:
// 示例 1
public class SynchronizedObject {
public synchronized void printString() { // 同步方法
System.out.println("Thread-" + Thread.currentThread().getName() + " begins.");
if (Thread.currentThread().getName().equals("a")) {
System.out.println("執行緒a suspend 了...");
Thread.currentThread().suspend();
}
System.out.println("Thread-" + Thread.currentThread().getName() + " is end.");
}
public static void main(String[] args) throws InterruptedException {
final SynchronizedObject object = new SynchronizedObject(); // 兩個執行緒使