多線程——java實現多線程的方式

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就是在操作系統中啓動了一個進程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章