Java的多線程及多線程編程

一、線程、進程、多線程的概念

Java 給多線程編程提供了內置的支持。

一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
進程:一個進程包括由操作系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到所有的非守護線程都結束運行後才能結束。

【注意】區分線程和進程的概念
進程:應用程序的執行實例,有獨立的內存空間和系統資源
線程:CPU調度和分派的基本單位,進程中執行運算的最小單位,可完成一個獨立的順序控制流程
進程和線程的關係
(1)一個線程只能屬於一個進程,而一個進程可以有多個線程,但至少有一個線程。線程是操作系統可識別的最小執行和調度單位
(2)資源分配給進程,同一進程的所有線程共享該進程的所有資源同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。但是每個線程擁有自己的棧段,棧段又叫運行時段,用來存放所有局部變量和臨時變量。
(3)處理機分給線程,即真正在處理機上運行的是線程
(4)線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。

多線程是多任務的一種特別的形式,但多線程使用了更小的資源開銷。
多線程能滿足程序員編寫高效率的程序來達到充分利用 CPU 的目的。
【注意】多線程就是分時利用 CPU,宏觀上讓所有線程一起執行 ,也叫併發

二、一個線程的生命週期(5種狀態)

線程是一個動態執行的過程,它也有一個從產生到死亡的過程。
下圖是一個線程完整的生命週期:
在這裏插入圖片描述
新建狀態: 使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。

就緒狀態:當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。

運行狀態:如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它可以變爲阻塞狀態、就緒狀態和死亡狀態。

阻塞狀態:如果一個線程執行了sleep(睡眠)suspend(掛起) 等方法,失去所佔用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分爲三種:

  1. 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。調用wait(),使該線程處於等待池(wait blocked pool),直到notify()/notifyAll(),線程被喚醒被放到鎖定池(lock blocked pool ),釋放同步鎖使線程回到可運行狀態(Runnable)。
  2. 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因爲同步鎖被其他線程佔用)。對Running狀態的線程加同步鎖(Synchronized)使其進入(lock blocked pool ),同步鎖被釋放進入可運行狀態(Runnable)。
  3. 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。

死亡狀態:
一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。

三、線程的優先級

每一個 Java 線程都有一個優先級,這樣有助於操作系統確定線程的調度順序。

Java 線程的優先級是一個整數,其取值範圍是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默認情況下,每一個線程都會分配一個優先級 NORM_PRIORITY(5)。

具有較高優先級的線程對程序更重要,並且應該在低優先級的線程之前分配處理器資源。但是,線程優先級不能保證線程執行的順序,而且非常依賴於平臺

四、創建一個線程

Java 提供了三種創建線程的方法:

  1. 通過實現 Runnable接口
  2. 通過繼承 Thread 類本身;
  3. 通過 Callable 和 Future 創建線程。

(1) 通過實現 Runnable 接口來創建線程

創建一個線程,最簡單的方法是創建一個實現 Runnable 接口的類。
爲了實現 Runnable,一個類只需要執行一個方法調用 run(),聲明如下:

public void run()

可以重寫該方法,重要的是理解的 run() 可以調用其他方法,使用其他類,並聲明變量,就像主線程一樣。
在創建一個實現 Runnable 接口的類之後,你可以在類中實例化一個線程對象。

Thread 定義了幾個構造方法,下面的這個是我們經常使用的:

Thread(Runnable threadOb,String threadName);

這裏,threadOb 是一個實現 Runnable 接口的類的實例,並且 threadName 指定新線程的名字。

新線程創建之後,你調用它的 start() 方法它纔會運行。

void start();

下面是一個創建線程並開始讓它執行的實例:

//實例
class RunnableDemo implements Runnable {
   private Thread t;
   private String threadName;
   
   RunnableDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 讓線程睡眠一會
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}
 
public class TestThread {
   public static void main(String args[]) {
      RunnableDemo R1 = new RunnableDemo( "Thread-1");
      R1.start();
      
      RunnableDemo R2 = new RunnableDemo( "Thread-2");
      R2.start();
   }   
}

運行結果如下:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

(2)通過繼承Thread來創建線程

創建一個線程的第二種方法是創建一個新的類,該類繼承 Thread 類,然後創建一個該類的實例。
繼承類必須重寫 run() 方法,該方法是新線程的入口點。它也必須調用 start() 方法才能執行。
該方法儘管被列爲一種多線程實現方式,但是本質上也是實現了 Runnable 接口的一個實例。

//實例
class ThreadDemo extends Thread {
   private Thread t;
   private String threadName;
   
   ThreadDemo( String name) {
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 讓線程睡眠一會
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start () {
      System.out.println("Starting " +  threadName );
      if (t == null) {
         t = new Thread (this, threadName);
         t.start ();
      }
   }
}
 
public class TestThread {
   public static void main(String args[]) {
      ThreadDemo T1 = new ThreadDemo( "Thread-1");
      T1.start();
      
      ThreadDemo T2 = new ThreadDemo( "Thread-2");
      T2.start();
   }   
}

運行結果如下:

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

—Thread 方法

下面列出Thread類的一些重要方法:
在這裏插入圖片描述
測試線程是否處於活動狀態。 上述方法是被Thread對象調用的。下面的方法是Thread類的靜態方法。
在這裏插入圖片描述
實例參考鏈接:https://www.runoob.com/java/java-multithreading.html

(3)通過 Callable 和 Future 創建線程

  1. 創建 Callable 接口的實現類,並實現 call() 方法,該 call() 方法將作爲線程執行體,並且有返回值。
  2. 創建 Callable 實現類的實例,使用 FutureTask 類來包裝 Callable 對象,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。
  3. 使用 FutureTask 對象作爲 Thread 對象的 target 創建並啓動新線程。
  4. 調用 FutureTask 對象的 get() 方法來獲得子線程執行結束後的返回值。
//實例
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  
    {  
        CallableThreadTest ctt = new CallableThreadTest();  
        FutureTask<Integer> ft = new FutureTask<>(ctt);  
        for(int i = 0;i < 100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);  
            if(i==20)  
            {  
                new Thread(ft,"有返回值的線程").start();  
            }  
        }  
        try  
        {  
            System.out.println("子線程的返回值:"+ft.get());  
        } catch (InterruptedException e)  
        {  
            e.printStackTrace();  
        } catch (ExecutionException e)  
        {  
            e.printStackTrace();  
        }  
  
    }
    @Override  
    public Integer call() throws Exception  
    {  
        int i = 0;  
        for(;i<100;i++)  
        {  
            System.out.println(Thread.currentThread().getName()+" "+i);  
        }  
        return i;  
    }  
}

五、創建線程的三種方式的對比

  1. 採用實現 Runnable、Callable 接口的方式創建多線程時,線程類只是實現了 Runnable 接口或 Callable 接口,還可以繼承其他類。
  2. 使用繼承 Thread 類的方式創建多線程時,編寫簡單,如果需要訪問當前線程,則無需使用 Thread.currentThread() 方法,直接使用 this 即可獲得當前線程。

六、線程的幾個主要概念

在多線程編程時,你需要了解以下幾個概念:

1、線程同步:即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作, 其他線程才能對該內存地址進行操作,而其他線程又處於等待狀態,實現線程同步的方法有很多,臨界區對象就是其中一種。
學習鏈接:
https://blog.csdn.net/ETalien_/article/details/86552623
https://www.cnblogs.com/XHJT/p/3897440.html
https://baike.baidu.com/item/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5

2、線程間通信
①同步;②while輪詢的方式;③wait/notify機制;④管道通信。
具體內容參考下面第一個鏈接。
學習鏈接:
https://www.cnblogs.com/hapjin/p/5492619.html
https://www.jianshu.com/p/9218692cb209
https://blog.csdn.net/z714405489/article/details/82814133

3、線程死鎖
學習鏈接:
https://blog.csdn.net/ls5718/article/details/51896159
https://www.cnblogs.com/xrq730/p/4853713.html
https://www.jianshu.com/p/68c0fef7b63e

4、線程控制:掛起、停止和恢復

補充一:線程狀態

在這裏插入圖片描述

線程共包括以下 5 種狀態:

  1. 新建狀態(New): 線程對象被創建後,就進入了新建狀態。例如,Thread thread = new Thread()。生成線程對象,並沒有調用該對象的 start 方法,這是線程處於創建狀態。
  2. 就緒狀態(Runnable): 也被稱爲“可執行狀態”。線程對象被創建後,其它線程調用了該對象的start()方法,從而來啓動該線程,但是此時線程調度程序還沒有把該線程設置爲當前線程,此時處於就緒狀態。在線程運行之後,從等待或者睡眠中回來之後,也會處於就緒狀態。例如,thread.start()。處於就緒狀態的線程,隨時可能被CPU調度執行。
  3. 運行狀態(Running): 線程獲取CPU權限進行執行。線程調度程序將處於就緒狀態的線程設置爲當前線程,此時線程就進入了運行狀態,開始運行 run 函數當中的代碼。需要注意的是,線程只能從就緒狀態進入到運行狀態
  4. 阻塞狀態(Blocked): 阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
    —(1)等待阻塞 – 通過調用線程的wait()方法,讓線程等待某工作的完成。
    —(2)同步阻塞 – 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態。
    — (3) 其他阻塞 – 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
    線程正在運行的時候,被暫停,通常是爲了等待某個時間的發生(比如說某項資源就緒)之後再繼續運行。sleep,suspend,wait 等方法都可以導致線程阻塞。
  5. 死亡狀態(Dead): 線程執行完了或者因異常退出了run()方法,該線程結束生命週期。如果一個線程的 run 方法執行結束或者調用 stop 方法後,該線程就會死亡。對於已經死亡的線程,無法再使用 start 方法令其進入就緒。

補充二:Thread類中和start()和run()的區別

每個線程都是通過某個特定 Thread 對象所對應的方法 run() 來完成其操作的,方法 run() 稱爲線程體。通過調用 Thread 類的 start() 方法來啓動一個線程。

先調用 start 後調用 run,這麼麻煩,爲了不直接調用 run
因爲就是爲了實現多線程的優點,沒這個 start 不行。

1.start() 方法來啓動線程,真正實現了多線程運行。這時無需等待 run 方法體代碼執行完畢,可以直接繼續執行下面的代碼;通過調用 Thread 類的 start() 方法來啓動一個線程, 這時此線程是處於就緒狀態, 並沒有運行。 然後通過此 Thread 類調用方法 run() 來完成其運行操作的, 這裏方法 run() 稱爲線程體,它包含了要執行的這個線程的內容, run 方法運行結束, 此線程終止。然後 CPU 再調度其它線程。
2.run() 方法當作普通方法的方式調用。程序還是要順序執行,要等待 run 方法體執行完畢後,纔可繼續執行下面的代碼; 程序中只有主線程——這一個線程, 其程序執行路徑還是隻有一條, 這樣就沒有達到寫線程的目的。

【注意】多線程就是分時利用 CPU,宏觀上讓所有線程一起執行 ,也叫併發。

補充三:java的多線程,Object類的線程方法

notify() :通知一個在對象上等待的線程,使其從wait()返回,而返回的前提是該線程獲取到了對象的鎖。

notifyAll(): 通知所有等待在該對象上的線程。

wait():調用該方法的線程進入WAITING狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意,調用wait()方法後,會釋放對象的鎖。

wait(long) :超時等待一段時間,這裏的參數是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回。

wait(long, int) : 對於超時時間更細粒度的控制,可以達到毫秒。

參考鏈接:
[1] https://www.runoob.com/java/java-multithreading.html
[2] https://www.cnblogs.com/wxd0108/p/5479442.html

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