java-19 三種多線程的實現方式

本文來自:https://blog.csdn.net/taojin12/article/details/85544403

博主:taojin12

------

【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()方法是準備執行,真正的執行要看操作系統的臉色。

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