1、線程和進程
要理解多線程,我們必須首先弄清楚線程和進程的概念。在上一篇博文總已經較爲詳細的介紹過,本篇博文只做總結。
進程就是運行的程序,每個進程都有獨立的代碼和數據空間(進程上下文),進程間的切換會有較大的開銷,一個進程包含1–n個線程。
線程是程序執行的最小單位,同一類線程共享代碼和數據空間,每個線程有獨立的運行棧和程序計數器(PC),線程切換開銷小。
線程和進程可以分爲五個階段:創建、就緒、運行、阻塞、終止。
多進程是指操作系統能同時運行多個任務(程序)。
多線程是指在同一程序中有多個順序流在執行。
2、Java線程的五種基本狀態
關於Java中線程的生命週期,首先看一下下面這張較爲經典的圖:
上圖中基本上囊括了Java中多線程各重要知識點。主要包括:
新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();
就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;
運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;
阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:
(1)等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;
(2)同步阻塞 – 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;
(3)其他阻塞 – 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
3、Java多線程實現方式
Java多線程實現方式主要有三種:繼承Thread類、實現Runnable接口和使用ExecutorService、Callable、Future實現有返回結果的多線程。其中前兩種方式線程執行完後都沒有返回值,最後一種是帶返回值的。
3.1 繼承Thread類實現多線程
繼承Thread類的方法儘管被我列爲一種多線程實現方式,但Thread本質上也是實現了Runnable接口的一個實例,它代表一個線程的實例,並且,啓動線程的唯一方法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啓動一個新線程,並執行run()方法。這種方式實現多線程很簡單,通過自己的類直接extend Thread,並複寫run()方法,就可以啓動新線程並執行自己定義的run()方法。例如:
import org.testng.annotations.Test;
/**
* 使用繼承Thread類方式實現
* new一個thread或者寫個thread子類,覆蓋它的run方法。(new 一個thread並覆蓋run方法實際上是匿名內部類的一種方式)
* 繼承Thread類,重寫該類的run()方法
*
*
* 使用Thread類模擬3個售票窗口共同賣10張火車票的程序
*
* 沒有共享數據,每個線程各賣10張火車票
*
*/
public class ThreadTest {
@Test
public void main(){
// //1、第一種實現方法
// //不共享數據,各自賣各自的,共30
// for(int i = 0; i < 3; i++) {
// System.out.println("---------------" + i + "---------");
// new Thread() {
// @Override
// public void run() {
// int tickets = 10;
// while (tickets > 0) {
// System.out.println(this.getName() + "賣出第【" + tickets-- + "】張火車票");
// }
// }
// }.start();
// }
//2、第一種實現方法
//2.1 不共享數據,各自賣各自的,共30
// for(int i = 0; i < 3; i++){
// System.out.println(Thread.currentThread().getName() + "--------" + i);
// //創建一個新的線程 myThread 此線程進入新建狀態
// Thread myThread = new MyThread();
// //調用start()方法使得線程進入就緒狀態
// //此時此線程並不一定會馬上得以執行,這取決於CPU調度時機
// //CPU調度就緒狀態中的哪個線程具有一定的隨機性
// myThread.start();
//
// //new Thread(new MyThread()).start();
// }
// 1.2 不共享數據,各自賣各自的,共30
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
public class MyThread extends Thread{
private int tickets = 10;//每個線程都擁有10張票
// run()方法的方法體代表了線程需要完成的任務,稱之爲線程執行體
@Override
public void run() {
while(tickets > 0){
System.out.println(this.getName() + ": 賣出第【" + tickets-- + "】張火車票");
}
}
}
}
結果:
Thread-0: 賣出第【10】張火車票
Thread-0: 賣出第【9】張火車票
Thread-0: 賣出第【8】張火車票
Thread-0: 賣出第【7】張火車票
Thread-0: 賣出第【6】張火車票
Thread-1: 賣出第【10】張火車票
Thread-2: 賣出第【10】張火車票
Thread-1: 賣出第【9】張火車票
Thread-0: 賣出第【5】張火車票
Thread-1: 賣出第【8】張火車票
Thread-2: 賣出第【9】張火車票
Thread-1: 賣出第【7】張火車票
Thread-0: 賣出第【4】張火車票
Thread-1: 賣出第【6】張火車票
Thread-2: 賣出第【8】張火車票
Thread-1: 賣出第【5】張火車票
Thread-1: 賣出第【4】張火車票
Thread-1: 賣出第【3】張火車票
Thread-0: 賣出第【3】張火車票
Thread-0: 賣出第【2】張火車票
Thread-0: 賣出第【1】張火車票
Thread-1: 賣出第【2】張火車票
Thread-2: 賣出第【7】張火車票
Thread-1: 賣出第【1】張火車票
Thread-2: 賣出第【6】張火車票
Thread-2: 賣出第【5】張火車票
Thread-2: 賣出第【4】張火車票
Thread-2: 賣出第【3】張火車票
Thread-2: 賣出第【2】張火車票
Thread-2: 賣出第【1】張火車票
如上所示,繼承Thread類,通過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體代表了線程需要完成的任務,稱之爲線程執行體。當創建此線程類對象時一個新的線程得以創建,並進入到線程新建狀態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不一定會馬上得以執行,這取決於CPU調度時機。
從結果可以看到,每個線程分別對應10張電影票,之間並無任何關係,這就說明每個線程之間是平等的,沒有優先級關係,因此都有機會得到CPU的處理。但是結果顯示這三個線程並不是依次交替執行,而是在三個線程同時被執行的情況下,有的線程被分配時間片的機會多,票被提前賣完,而有的線程被分配時間片的機會比較少,票遲一些賣完。
可見,利用擴展Thread類創建的多個線程,雖然執行的是相同的代碼,但彼此相互獨立,且各自擁有自己的資源,互不干擾。
3.2 通過實現Runnable接口來創建多線程
實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象。例如:
public class RunableTest {
private static int tickets = 10;//每個線程都擁有10張票
public static void main(String[] args) {
// 1、第一種方法
// for(int i = 0; i < 3; i++) {
// System.out.println(Thread.currentThread().getName());
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// while (tickets > 0) {
// System.out.println(Thread.currentThread().getName() + ": 賣出第【" + tickets-- + "】張火車票");
// }
// }
// }).start();
// }
//2、第二種方法
for(int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName());
// 創建一個Runnable實現類的對象
Runnable myRunnable = new MyThread();
// 將myRunnable作爲Thread target創建新的線程
Thread thread = new Thread(myRunnable);
// 調用start()方法使得線程進入就緒狀態
thread.start();
// new Thread(new ThreadTest.MyThread()).start();
}
//1.2
// Runnable runnable = new MyThread();
// new Thread(runnable).start();
// new Thread(runnable).start();
// new Thread(runnable).start();
}
public static class MyThread implements Runnable{
public void run() {
while(tickets > 0){
System.out.println(Thread.currentThread().getName() + ": 賣出第【" + tickets-- + "】張火車票");
}
}
}
}
結果:
Thread-0: 賣出第【10】張火車票
Thread-0: 賣出第【8】張火車票
Thread-1: 賣出第【9】張火車票
Thread-1: 賣出第【5】張火車票
Thread-1: 賣出第【4】張火車票
Thread-1: 賣出第【3】張火車票
Thread-1: 賣出第【2】張火車票
Thread-1: 賣出第【1】張火車票
Thread-0: 賣出第【6】張火車票
Thread-2: 賣出第【7】張火車票
上面的程序中,創建了三個線程,每個線程調用的是同一個MyThread對象中的run()方法,訪問的是同一個對象中的變量(tickets)的實例,這個程序滿足了我們的需求。程序在內存中僅創建了一個資源,而新建的三個線程都是基於訪問這同一資源的,並且由於每個線程上所運行的是相同的代碼,因此它們執行的功能也是相同的。
我們可以看出,通過實現Runnable接口,我們實現了多個線程去處理同一個資源。我們只能創建一個資源對象,但要創建多個線程去處理這同一個資源對象,並且每個線程上所運行的是相同的程序代碼。
可見,如果現實問題中要求必須創建多個線程來執行同一任務,而且這多個線程之間還將共享同一個資源,那麼就可以使用實現Runnable接口的方式來創建多線程程序。而這一功能通過擴展Thread類是無法實現的。實現Runnable接口相對於擴展Thread類來說,具有無可比擬的優勢。這種方式不僅有利於程序的健壯性,使代碼能夠被多個線程共享,而且代碼和數據資源相對獨立,從而特別適合多個具有相同代碼的線程去處理同一資源的情況。這樣一來,線程、代碼和數據資源三者有效分離,很好地體現了面向對象程序設計的思想。因此,幾乎所有的多線程程序都是通過實現Runnable接口的方式來完成的。
3.3 3、使用ExecutorService、Callable、Future實現有返回結果的多線程
ExecutorService、Callable、Future這個對象實際上都是屬於Executor框架中的功能類。想要詳細瞭解Executor框架的可以訪問http://www.javaeye.com/topic/366591 ,這裏面對該框架做了很詳細的解釋。返回結果的線程是在JDK1.5中引入的新特徵,確實很實用,有了這種特徵我就不需要再爲了得到返回值而大費周折了,而且即便實現了也可能漏洞百出。
可返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。執行Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了,再結合線程池接口ExecutorService就可以實現傳說中有返回結果的多線程了。下面提供了一個完整的有返回結果的多線程測試例子,在JDK1.5下驗證過沒問題可以直接使用。代碼如下:
import java.util.concurrent.*;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
/**
* 有返回值的線程
*/
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序開始運行----");
Date date1 = new Date();
int taskSize = 5;
// 創建一個線程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 創建多個有返回值的任務
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 執行任務並獲取Future對象
Future f = pool.submit(c);
// System.out.println(">>>" + f.get().toString());
list.add(f);
}
// 關閉線程池
pool.shutdown();
// 獲取所有併發任務的運行結果
for (Future f : list) {
// 從Future對象上獲取任務的返回值,並輸出到控制檯
System.out.println(">>>" + f.get().toString());
}
Date date2 = new Date();
System.out.println("----程序結束運行----,程序運行時間【"
+ (date2.getTime() - date1.getTime()) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任務啓動");
Date dateTmp1 = new Date();
Thread.sleep(1000);
Date dateTmp2 = new Date();
long time = dateTmp2.getTime() - dateTmp1.getTime();
System.out.println(">>>" + taskNum + "任務終止");
return taskNum + "任務返回運行結果,當前任務時間【" + time + "毫秒】";
}
}
代碼說明:
上述代碼中Executors類,提供了一系列工廠方法用於創先線程池,返回的線程池都實現了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads) 創建固定數目線程的線程池。
public static ExecutorService newCachedThreadPool()創建一個可緩存的線程池,調用execute 將重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則創建一個新線程並添加到池中。終止並從緩存中移除那些已有 60 秒鐘未被使用的線程。
public static ExecutorService newSingleThreadExecutor() 創建一個單線程化的Executor。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 創建一個支持定時及週期性的任務執行的線程池,多數情況下可用來替代Timer類。
ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor後臺線程池還沒有完成Callable的計算,這調用返回Future對象的get()方法,會阻塞直到計算完成。
4、繼承Thread類和實現Runable接口比較
4.1 爲什麼Java要提供繼承Thread類和實現Runable接口來實現多線程線程
在Java中,類僅支持單繼承,也就是說,當定義一個新的類的時候,它只能擴展一個外部類。這樣,如果創建自定義線程類的時候是通過擴展 Thread類的方法來實現的,那麼這個自定義類就不能再去擴展其他的類,也就無法實現更加複雜的功能。因此,如果自定義類必須擴展其他的類,那麼就可以使用實現Runnable接口的方法來定義該類爲線程類,這樣就可以避免Java單繼承所帶來的侷限性。
還有一點最重要的就是使用實現Runnable接口的方式創建的線程可以處理同一資源,從而實現資源的共享。
4.2 實現Runnable接口相對於繼承Thread類好處
(1)適合多個相同程序代碼的線程去處理同一資源的情況,把虛擬CPU(線程)同程序的代碼,數據有效的分離,較好地體現了面向對象的設計思想。
(2)可以避免由於Java的單繼承特性帶來的侷限。我們經常碰到這樣一種情況,即當我們要將已經繼承了某一個類的子類放入多線程中,由於一個類不能同時有兩個父類,所以不能用繼承Thread類的方式,那麼,這個類就只能採用實現Runnable接口的方式了。
(3)有利於程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的。當多個線程的執行代碼來自同一個類的實例時,即稱它們共享相同的代碼。多個線程操作相同的數據,與它們的代碼無關。當共享訪問相同的對象是,即它們共享相同的數據。當線程被構造時,需要的代碼和數據通過一個對象作爲構造函數實參傳遞進去,這個對象就是一個實現了Runnable接口的類的實例。
值得注意的是:main方法其實也是一個線程。在java中所以的線程都是同時啓動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。在java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。因爲每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每一個JVM就是在操作系統中啓動了一個進程。