Java多執行緒(一) —— 執行緒的狀態詳解
一、多執行緒概述
1、 程序
- 是一個正在執行的程式。是程式在計算機上的一次執行活動。
- 每一個程序執行都有一個執行順序。該順序是一個執行路徑,或者叫一個控制單元。
- 系統以程序為基本單位進行系統資源的排程和分配。程式要執行,系統就在記憶體中為該程式分配一塊獨立的記憶體空間,載入程式程式碼和資源進行執行。
- 程式執行期間該記憶體空間不能被其他程序直接訪問。
2、執行緒
- 程序中的一個獨立的控制單元。執行緒在控制著程序的執行。只要程序中有一個執行緒在執行,程序就不會結束。
- 執行緒是程序內一次具體的執行任務。程式的執行具體是通過執行緒來完成的,所以一個程序中至少有一個執行緒。
- 執行緒是CPU排程的基本單位。
- 一個程序可以包含多個執行緒,這些執行緒共享資料空間和資源,但又分別擁有各自的執行堆疊和程式計數器。
其實HelloWrold 程式中main方法的執行,就是Java虛擬機器開啟的一個名為“main”的執行緒來執行程式程式碼。
3、多執行緒
在java虛擬機器啟動的時候會有一個java.exe的執行程式,也就是一個程序。該程序中至少有一個執行緒負責java程式的執行。而且這個執行緒執行的程式碼存在於main方法中。該執行緒稱之為主執行緒。JVM啟動除了執行一個主執行緒,還有負責垃圾回收機制的執行緒。像種在一個程序中有多個執行緒執行的方式,就叫做多執行緒。
4、多執行緒存在的意義
多執行緒的出現能讓程式產生同時執行效果。可以提高程式執行效率。
例如:在java.exe程序執行主執行緒時,如果程式程式碼特別多,在堆記憶體中產生了很多物件,而同時物件呼叫完後,就成了垃圾。如果垃圾過多就有可能是堆記憶體出現記憶體不足的現象,只是如果只有一個執行緒工作的話,程式的執行將會很低效。而如果有另一個執行緒幫助處理的話,如垃圾回收機制執行緒來幫助回收垃圾的話,程式的執行將變得更有效率。
5、計算機CPU的執行原理
我們電腦上有很多的程式在同時進行,就好像cpu在同時處理這所以程式一樣。但是,在一個時刻,單核的cpu只能執行一個程式。而我們看到的同時執行效果,只是cpu在多個程序間做著快速切換動作。
而cpu執行哪個程式,是毫無規律性的。這也是多執行緒的一個特性:隨機性。哪個執行緒被cpu執行,或者說搶到了cpu的執行權,哪個執行緒就執行。而cpu不會只執行一個,當執行一個一會後,又會去執行另一個,或者說另一個搶走了cpu的執行權。至於究竟是怎麼樣執行的,只能由cpu決定。
二、執行緒的生命週期及五種基本狀態
Java執行緒具有五中基本狀態
新建狀態(New):當執行緒物件對建立後,即進入了新建狀態,如:Thread t = new MyThread();
就緒狀態(Runnable):當呼叫執行緒物件的start()方法(t.start();),執行緒即進入就緒狀態。處於就緒狀態的執行緒,只是說明此執行緒已經做好了準備,隨時等待CPU排程執行,並不是說執行了t.start()此執行緒立即就會執行;
執行狀態(Running):當CPU開始排程處於就緒狀態的執行緒時,此時執行緒才得以真正執行,即進入到執行狀態。注:就緒狀態是進入到執行狀態的唯一入口,也就是說,執行緒要想進入執行狀態執行,首先必須處於就緒狀態中;
阻塞狀態(Blocked):處於執行狀態中的執行緒由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU呼叫以進入到執行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
- 等待阻塞 -- 執行狀態中的執行緒執行wait()方法,使本執行緒進入到等待阻塞狀態;
- 同步阻塞 -- 執行緒在獲取synchronized同步鎖失敗(因為鎖被其它執行緒所佔用),它會進入同步阻塞狀態;
- 其他阻塞 -- 通過呼叫執行緒的sleep()或join()或發出了I/O請求時,執行緒會進入到阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。
死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。
三、Java多執行緒的建立及啟動
- 繼承Thread類,重寫該類的run()方法;
- 實現Runnable介面,並重寫該介面的run()方法;
- 使用Callable和Future介面建立執行緒。
1.繼承Thread類,重寫該類的run()方法。
class MyThread extends Thread { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread myThread1 = new MyThread(); // 建立一個新的執行緒 myThread1 此執行緒進入新建狀態 Thread myThread2 = new MyThread(); // 建立一個新的執行緒 myThread2 此執行緒進入新建狀態 myThread1.start(); // 呼叫start()方法使得執行緒進入就緒狀態 myThread2.start(); // 呼叫start()方法使得執行緒進入就緒狀態 } } } }
如上所示,繼承Thread類,通過重寫run()方法定義了一個新的執行緒類MyThread,其中run()方法的方法體代表了執行緒需要完成的任務,稱之為執行緒執行體。當建立此執行緒類物件時一個新的執行緒得以建立,並進入到執行緒新建狀態。通過呼叫執行緒物件引用的start()方法,使得該執行緒進入到就緒狀態,此時此執行緒並不一定會馬上得以執行,這取決於CPU排程時機。
2.實現Runnable介面,並重寫該介面的run()方法,該run()方法同樣是執行緒執行體,建立Runnable實現類的例項,並以此例項作為Thread類的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
class MyRunnable implements Runnable { private int i = 0; @Override public void run() { for (i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
public class ThreadTest { public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Runnable myRunnable = new MyRunnable(); // 建立一個Runnable實現類的物件 Thread thread1 = new Thread(myRunnable); // 將myRunnable作為Thread target建立新的執行緒 Thread thread2 = new Thread(myRunnable); thread1.start(); // 呼叫start()方法使得執行緒進入就緒狀態 thread2.start(); } } } }
Thread和Runnable之間到底是什麼關係呢?
兩種方法建立執行緒的方法類似,不同的地方在於:
Thread thread = new MyThread(myRunnable);
那麼這種方式可以順利創建出一個新的執行緒麼?答案是肯定的。那麼此時的執行緒執行體到底是MyRunnable介面中的run()方法還是MyThread類中的run()方法呢?執行緒執行體是MyThread類中的run()方法。其實原因很簡單,因為Thread類本身也是實現了Runnable介面,而run()方法最先是在Runnable介面中定義的方法。所以執行時優先執行MyThread類中的run()方法。
Runnable介面中最先定義的run()方法:
public interface Runnable { public abstract void run(); }
Thread類中對於Runnable介面中run()方法的實現:
@Override public void run() { if (target != null) { target.run(); } }
也就是說,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable介面並重寫了run()方法的類中的run()方法。但是上述給到的列子中,由於多型的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了執行時型別即MyThread類中的run()方法。
3.使用Callable和Future介面建立執行緒。具體是建立Callable介面的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的物件,且以此FutureTask物件作為Thread物件的target來建立執行緒。
看著好像有點複雜,直接來看一個例子就清晰了。
public class ThreadTest { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 建立MyCallable物件 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable物件
for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask物件作為Thread物件的target建立新的執行緒 thread.start(); //執行緒進入到就緒狀態 } } System.out.println("主執行緒for迴圈執行完畢.."); try { int sum = ft.get(); //取得新建立的新執行緒中的call()方法返回的結果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { private int i = 0; // 與run()方法不同的是,call()方法具有返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; } }
在實現Callable介面中,此時不再是run()方法了,而是call()方法,此call()方法作為執行緒執行體,同時還具有返回值!在建立新的執行緒時,是通過FutureTask來包裝MyCallable物件,同時作為了Thread物件的target。
我們發現FutureTask類實際上是同時實現了Runnable和Future介面,由此才使得其具有Future和Runnable雙重特性。通過Runnable特性,可以作為Thread物件的target,而Future特性,使得其可以取得新建立執行緒中的call()方法的返回值。
執行下此程式,我們發現sum = 4950永遠都是最後輸出的。而“主執行緒for迴圈執行完畢..”則很可能是在子執行緒迴圈中間輸出。由CPU的執行緒排程機制,我們知道,“主執行緒for迴圈執行完畢..”的輸出時機是沒有任何問題的,那麼為什麼sum =4950會永遠最後輸出呢?原因在於通過ft.get()方法獲取子執行緒call()方法的返回值時,當子執行緒此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。
上述主要講解了三種常見的執行緒建立方式,對於執行緒的啟動而言,都是呼叫執行緒物件的start()方法,需要特別注意的是:不能對同一執行緒物件兩次呼叫start()方法。
四、Java多執行緒的就緒、執行和死亡狀態
就緒狀態轉換為執行狀態:當此執行緒得到處理器資源;
執行狀態轉換為就緒狀態:當此執行緒主動呼叫yield()方法或在執行過程中失去處理器資源。
執行狀態轉換為死亡狀態:當此執行緒執行緒執行體執行完畢或發生了異常。
此處需要特別注意的是:當呼叫執行緒的yield()方法時,執行緒從執行狀態轉換為就緒狀態,但接下來CPU排程就緒狀態中的哪個執行緒具有一定的隨機性,因此,可能會出現A執行緒呼叫了yield()方法後,接下來CPU仍然排程了A執行緒的情況。
由於實際的業務需要,常常會遇到需要在特定時機終止某一執行緒的執行,使其進入到死亡狀態。目前最通用的做法是設定一boolean型的變數,當條件滿足時,使執行緒執行體快速執行完畢。如:
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); } if(i == 40){ myRunnable.stopThread(); } } } } class MyRunnable implements Runnable { private boolean stop; @Override public void run() { for (int i = 0; i < 100 && !stop; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public void stopThread() { this.stop = true; } }
五、Java多執行緒的阻塞狀態與執行緒控制
引起Java執行緒阻塞的主要方法:
1.join()
join —— 讓一個執行緒等待另一個執行緒完成才繼續執行。如A執行緒執行緒執行體中呼叫B執行緒的join()方法,則A執行緒被阻塞,直到B執行緒執行完為止,A才能得以繼續執行。
為什麼要用join()方法
在很多情況下,主執行緒生成並啟動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了。
2.sleep()
sleep —— 讓當前的正在執行的執行緒暫停指定的時間,並進入阻塞狀態。在其睡眠的時間段內,該執行緒由於不是處於就緒狀態,因此不會得到執行的機會。即使此時系統中沒有任何其他可執行的執行緒,出於sleep()中的執行緒也不會執行。因此sleep()方法常用來暫停執行緒執行。
前面有講到,當呼叫了新建的執行緒的start()方法後,執行緒進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,如果希望這個新執行緒必然性的立即執行,直接呼叫原來執行緒的sleep(1)即可。
注:睡一個毫秒級夠了,因為CPU不會空閒,會切換到新建的執行緒。
3.後臺執行緒(Daemon Thread)
概念/目的:後臺執行緒主要是為其他執行緒(相對可以稱之為前臺執行緒)提供服務,或“守護執行緒”。如JVM中的垃圾回收執行緒。
生命週期:後臺執行緒的生命週期與前臺執行緒生命週期有一定關聯。主要體現在:當所有的前臺執行緒都進入死亡狀態時,後臺執行緒會自動死亡(其實這個也很好理解,因為後臺執行緒存在的目的在於為前臺執行緒服務的,既然所有的前臺執行緒都死亡了,那它自己還留著有什麼用...偉大啊 ! !)。
設定後臺執行緒:呼叫Thread物件的setDaemon(true)方法可以將指定的執行緒設定為後臺執行緒。
判斷執行緒是否是後臺執行緒:呼叫thread物件的isDeamon()方法。
注:main執行緒預設是前臺執行緒,前臺執行緒建立中建立的子執行緒預設是前臺執行緒,後臺執行緒中建立的執行緒預設是後臺執行緒。呼叫setDeamon(true)方法將前臺執行緒設定為後臺執行緒時,需要在start()方法呼叫之前。前天執行緒都死亡後,JVM通知後臺執行緒死亡,但從接收指令到作出響應,需要一定的時間。
4.改變執行緒的優先順序/setPriority():
每個執行緒在執行時都具有一定的優先順序,優先順序高的執行緒具有較多的執行機會。每個執行緒預設的優先順序都與建立它的執行緒的優先順序相同。main執行緒預設具有普通優先順序。
設定執行緒優先順序:setPriority(int priorityLevel)。引數priorityLevel範圍在1-10之間,常用的有如下三個靜態常量值:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5
獲取執行緒優先順序:getPriority()。
注:具有較高執行緒優先順序的執行緒物件僅表示此執行緒具有較多的執行機會,而非優先執行。
5.yield()執行緒讓步
yield()的基本作用:暫停當前正在執行的執行緒物件讓出CPU資源,將當前執行緒從執行狀態轉換到就緒狀態並執行其他執行緒。同時,yield()方法還與執行緒優先順序有關,當某個執行緒呼叫yiled()方法從執行狀態轉換到就緒狀態後,CPU從就緒狀態執行緒佇列中只會選擇與該執行緒優先順序相同或優先順序更高的執行緒去執行。使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。
注意:yield()從未導致執行緒轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致執行緒從執行狀態轉到可執行(就緒)狀態,但有可能沒有效果。
join()例項:
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); try { thread.join(); // main執行緒需要等待thread執行緒執行完後才能繼續執行 } catch (InterruptedException e) { e.printStackTrace(); } } } } } class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
sleep()例項:
public class ThreadTest { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread thread = new Thread(myRunnable); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { thread.start(); try { Thread.sleep(1); // 使得thread必然能夠馬上得以執行 } catch (InterruptedException e) { e.printStackTrace(); } } } } } class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
設定後臺程序例項:
public class ThreadTest { public static void main(String[] args) { Thread myThread = new MyThread(); for (int i = 0; i < 100; i++) { System.out.println("main thread i = " + i); if (i == 20) { myThread.setDaemon(true); //設定為後臺守護執行緒 myThread.start(); } } } } class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println("i = " + i); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } }
setPriority()例項:
public class ThreadTest { public static void main(String[] args) { Thread myThread = new MyThread(); for (int i = 0; i < 100; i++) { System.out.println("main thread i = " + i); if (i == 20) { myThread.setPriority(Thread.MAX_PRIORITY); myThread.start(); } } } } class MyThread extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println("i = " + i); } } }
yeild()例項:
public class ThreadTest { public static void main(String[] args) { Thread myThread1 = new MyThread1(); Thread myThread2 = new MyThread2(); myThread1.setPriority(Thread.MAX_PRIORITY); myThread2.setPriority(Thread.MIN_PRIORITY); for (int i = 0; i < 100; i++) { System.out.println("main thread i = " + i); if (i == 20) { myThread1.start(); myThread2.start(); Thread.yield(); } } } } class MyThread1 extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println("myThread 1 -- i = " + i); } } } class MyThread2 extends Thread { public void run() { for (int i = 0; i < 100; i++) { System.out.println("myThread 2 -- i = " + i); } } }
參考連結:
http://www.cnblogs.com/lwbqqyumidi/p/3804883.html
http://www.cnblogs.com/lwbqqyumidi/p/3817517.html
http://www.cnblogs.com/lwbqqyumidi/p/3821389.html