1. 程式人生 > >【java多執行緒程式設計】三種多執行緒的實現方式

【java多執行緒程式設計】三種多執行緒的實現方式

文章目錄

前言

      在java語言最大的特點是支援多執行緒的開發(也是為數不多支援多執行緒的程式語言),所以在整個的java技術的學習裡面,如果你不能夠對多執行緒的概念有一個全面並且細緻的瞭解,則在日後進行一些專案設計的過程中尤其是併發訪問設計過程之中就會出現嚴重的技術缺陷。

      如果要想理解我們的執行緒,那麼首先就需要了解一下程序的概念,在傳統的DOS系統的時代,其本身有特徵:如果你的電腦上出現了病毒,那麼我們所有的程式無法執行,因為傳統的DOS採用的是單程序處理,而單程序處理的最大特點:在同一時間段上只允許一個程式在執行。

      那麼後來到了windows的時代就開啟了多程序的設計,於是就表示在一個時間段上可以同時執行多個程式,並且這些程式將進行資源的輪流搶佔。所以在同一時段上會有多個程式依次執行,但是在同一個時間點上只會有一個進行執行,而後來到了多核的CPU,由於可以處理的CPU多了,那麼即便有再多的程序出現,也可以比單核CPU處理的速度有多提升。

Java是多執行緒程式語言,所以Java在進行併發訪問處理的時候,可以得到更高的處理效能。

程序與執行緒

      如果想要在java之中實現多執行緒的定義,那麼就需要有一個專門的執行緒的主體類進行執行緒的執行任務的定義,
而這主體類的定義是有要求的,必須實現特定的介面或者繼承特定的父類才可以完成。

繼承Thread類,實現多執行緒

      java裡面提供了一個java.lang.Thread的程式類,那麼一個類只要繼承了此類就表示這個類為執行緒的主體類,
但是並不是說這個類就可以實現多執行緒處理了,因為還需要覆寫Thread類中提供的一個run()方法(public void run()),而這個方法就屬於執行緒的主方法。
範例:

class MyThread extends Thread {//執行緒主體類
    private String title;
    public MyThread(String title) {
        this.title = title;
    }
    @Override
    public void run() {//執行緒的主體方法
        for(int x = 0; x < 10 ; x++) {
            System.out.println(this.title + "執行,x = " + x);
        }
    }
}

      多執行緒要執行的功能,都應該在run()方法中進行定義,但是需要說明的是:
在正常情況下,如果要想使用一個類中的方法,那麼肯定要產生例項化物件,而後去呼叫類中提供的方法,但是run()方法不能直接呼叫的,因為這牽扯到一個作業系統的資源排程問題,所以要想啟動多執行緒必須使用start()方法。
範例:

public class ThreadDemo {
    public static void main(String[] args) {
        new MyThread("執行緒A").start();
        new MyThread("執行緒B").start();
        new MyThread("執行緒C").start();
    }
}

      通過此時的呼叫你可以發現,雖然呼叫了start()方法,但是最終執行的run()方法,並且所有的執行緒物件都是交替執行的。執行順序不可控。

FAQ 為什麼多執行緒的啟動不直接使用run()方法而必須使用Thread類中start()方法呢?

      如果想清楚這個問題,最好的做法是檢視一下start()方法的實現操作,可以直接通過原始碼觀察。

  public synchronized void start() {

        if (threadStatus != 0) // 判斷執行緒的狀態
            throw new IllegalThreadStateException();  // 丟擲一個異常
        group.add(this);
        boolean started = false;
        try {
            start0();   //  在start()中呼叫了start0()
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
 private native void start0();           

      發現start()方法裡面會丟擲一個"IllegalThreadStateException()"異常類物件,但整個程式並沒有進行try…catch處理,因為該異常一定是RuntimeException的子類,每一個執行緒類的物件只允許啟動一次,如果重複啟動就會丟擲異常,例如:下面的程式碼就會丟擲異常。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread mt = new MyThread("執行緒A");
        mt.start();
        mt.start();  //重複進行執行緒的啟動
    }
}
Exception in thread "main" java.lang.IllegalThreadStateException

      在JAVA程式執行的過程之中考慮到對於不同層次開發者的需求,所以其支援有本地的作業系統函式呼叫,而這項技術就被稱為JNI(Java Native Inteface)技術,但是JAVA開發過程之中並不推薦這樣使用,利用這項技術可以使用一些作業系統提供的底層函式進行一些特殊的處理,而在Thread類裡面提供了start()就表示需要將此方法依賴於不同的作業系統實現。
在這裡插入圖片描述
任何情況下,只要定義了多執行緒,多執行緒的啟動永遠只有一種方案:Thread類中的start()方法。

基於Runnable介面實現多執行緒

      雖然可以通過Thread類的繼承來實現多執行緒的定義,但是在Java程式裡面對於繼承永遠都是存在有單繼承侷限的,所以在Java裡面又提供有第二種多執行緒的主體定義結構形式:實現java.lang.Runnable介面,此介面定義如下:

@FunctionalInterface    // 從JDK1.8引入了Lambda 表示式之後就變為了函式式介面
public interface Runnable {
  public void run();
}

範例:通過Runnable 實現多執行緒的主體類

class MyThread implements Runnable {//執行緒主體類
    private String title;
    public MyThread(String title) {
        this.title = title;
    }
    @Override
    public void run() {//執行緒的主體方法
        for(int x = 0; x < 10 ; x++) {
            System.out.println(this.title + "執行,x = " + x);
        }
    }
}

      但是此時由於不在繼承Thread父類了,那麼對於此時的MyThread類中也就不在支援有start()這個繼承方法,可是不使用Thread.start()方法是無法進行多執行緒啟動的,那麼這個時候就需要去觀察一下Thread類所提供的構造方法了。
- 構造方法:public Thread(Runnable target);
範例:啟動多執行緒

public class ThreadDemo {
    public static void main(String[] args) {
        Thread threadA = new Thread(new MyThread("執行緒A"));
        Thread threadB = new Thread(new MyThread("執行緒B"));
        Thread threadC = new Thread(new MyThread("執行緒C"));
        threadA.start();
        threadB.start();
        threadC.start();
    }
}

      這個時候的多執行緒實現裡面可以發現,由於只是實現了Runnable介面物件,所以此時執行緒主體類上不再有單繼承侷限了,那麼這樣的設計才是一個標準的設計。
      可以發現從JDK1.8開始,Runnable介面使用了函式式介面定義,所以也可以直接利用Lambda表示式進行執行緒類的實現定義。
範例:利用Lambda實現多執行緒定義

public class ThreadDemo {
    public static void main(String[] args) {
        for( int x = 0; x < 3 ; x ++) {
            String title = "執行緒物件-" + x;
                Runnable runnable = () ->{
                    for(int y = 0; y < 10 ; y ++) {
                        System.out.println(title + "執行,y = " + y);
                }
            };
                new Thread(runnable).start();
        }
    }
}

在以後的開發中對於多執行緒的實現,優先考慮Runnable介面實現,並且永恆都是通過Thread類物件啟動多執行緒。

Thread 與 Runnable 的關係

      經過一系列的分析之後可以發現,在多執行緒的實現過程之中已經有了兩種做法:Thread類、Runnable介面,如果從程式碼的結構本身來講肯定使用Runnable是最方便的,因為其可以避免單繼承的侷限,同時也可以更好的進行功能的擴充。

      但是從結構上也需要觀察Thread與Runnable的聯絡,開啟Thread類的定義:

 public class Thread extends Object implements Runnable{}

發現Thread類也是Runnable 介面的子類,那麼在之前繼承Thread類的時候實際上覆寫的還是Runnable的方法。
在這裡插入圖片描述
      多執行緒的設計之中,使用了代理設計模式的結構,使用者自定義的執行緒主體只是負責專案核心功能的實現,而所有的輔助實現全部交給Thread類來處理。

      在進行Thread啟動多執行緒的時候呼叫的是start()方法,而後找到的是run()方法。當通過Thread類的構造方法傳遞了一個Runnable介面物件的時候,那麼該介面物件將被Thread中target的屬性儲存,在start()方法執行的時候會呼叫Thread類中的run方法,而這個run()方法去呼叫Runnable介面子類被覆寫過的run()方法。

      多執行緒開發的本質實質上是在於多個執行緒可以進行統一資源的搶佔,那麼Thread主要描述的是執行緒,那麼資源的描述是通過Runnable完成的
在這裡插入圖片描述

範例:利用賣票程式來實現多個執行緒的資源併發訪問。

class MyThread1 implements  Runnable {
   private int ticket = 5;
    @Override
    public void run() {
        for( int x = 0 ; x < 100 ; x ++) {
             if(this.ticket > 0) {
                 System.out.println("賣票,ticket = " +this.ticket --);
             }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
     MyThread1 mt = new MyThread1();
     new Thread(mt).start();
     new Thread(mt).start();
     new Thread(mt).start();
    }
}

通過記憶體分析圖來分析本程式的執行結構。
在這裡插入圖片描述

Callable實現多執行緒

      從最傳統的開發來講如果要進行多執行緒的實現肯定依靠的就是Runnable,但是Runnable介面有一個缺點:當執行緒執行完畢後,我們無法獲取一個返回值,所以從JDK1.5之後就提出了一個新的執行緒實現介面:java.util.concurrent.Callable介面。首先觀察這個介面的定義:

@FunctionalInterface
public interface Callable<V> {
 public V call() throws Exception;
}

      可以發現Callbale定義的時候可以設定一個泛型,此泛型的型別就是返回資料的型別,這樣的的好處是可以避免向下轉行所帶來的安全隱患。
在這裡插入圖片描述
範例:Callable多執行緒的實現

class MyThread2 implements Callable<String> {
    @Override
    public String call() throws Exception {
        for ( int x = 0 ; x < 10 ; x ++ ) {
            System.out.println("******執行緒執行,x = " + x);
        }
        return "執行緒執行完畢!";
    }
}
public class demo {
    public static void main(String[] args) throws Exception{
        FutureTask futureTask = new FutureTask(new MyThread2());
        new Thread(futureTask).start();
        System.out.println("執行緒返回值:" + futureTask.get());
    }
}

面試題:請解釋Runnable 與 Callable的區別:

  • Runnable是在JDK1.0的時候提出的多執行緒的實現介面,而Callable是在JDK1.5之後提出的;
  • java.lang.Runnable 介面之中只提供了一個run()方法,並且沒有返回值;
  • java.util.concurrent.Callable介面提供有call(),可以有返回值;

執行緒執行狀態

      對於多執行緒的開發而言,編寫程式的過程之中總是按照:定義執行緒主體類,而後通過Thread類進行執行緒的啟動,但是並不意味著你呼叫了start()方法,執行緒就已經開始運行了,因為整體的執行緒處理有自己的一套執行的狀態。

在這裡插入圖片描述
1、任何一個執行緒的物件都應該使用Thread類進行封裝,所以執行緒的啟動使用的是start(),但是啟動的時候實際上若干個執行緒都將進入到一種就緒狀態,現在並沒有執行。
2、進入到就緒狀態之後就需要等待進行資源排程,當某一個執行緒排程成功之後則進入到執行狀態(run()方法),但是所有的執行緒不可能一直持續執行下去,中間需要產生一些暫停狀態,例如:某個執行緒執行一段時間之後就需要讓出資源,而後這個執行緒就進入到阻塞狀態,隨後重新迴歸到就緒狀態;
3、當run()方法執行完畢之後,實際上該執行緒的主要任務也就結束了,那麼此時就可以直接進入到停止狀態。

另外:start()方法是準備執行,真正的執行要看作業系統的臉色。