【Java】多線程初探


 參考書籍:《Java核心技術 卷Ⅰ 》
 

Java的線程狀態

 
從操作系統的角度看,線程有5種狀態:創建, 就緒, 運行, 阻塞, 終止(結束)。如下圖所示
 
 

 

而Java定義的線程狀態有: 創建(New), 可運行(Runnable), 阻塞(Blocked), 等待(Waiting), 計時等待(Time waiting) 被終止(Terminated)。
 
那么相比起操作系統的線程狀態, Java定義的線程狀態該如何解讀呢? 如下:
1. Java的阻塞、 等待、 計時等待都屬於操作系統中定義的阻塞狀態,不過做了進一步的划分,阻塞(Blocked)是試圖獲得對象鎖(不是java.util.concurrent庫中的鎖),而對象鎖暫時被其他線程持有導致的;等待(Waiting)則是調用Object.wait,Thread.join或Lock.lock等方法導致的;計時等待(Time waiting)則是在等待的方法中引入了時間參數進入的狀態,例如sleep(s)
2. Java的Runnable狀態實際上包含了操作系統的就緒和運行這兩種狀態, 但並沒有專門的標識進行區分,而是統一標識為Runnable
 
獲取當前線程的狀態和名稱
currentThread()是Thread類的一個靜態方法,它返回的是當前線程對象
對某個線程對象有以下方法:
  • getState方法:返回該線程的狀態,可能是NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TEMINATED之一
  • getName: 返回線程名稱
  • getPriority: 返回線程優先級
下面的例子中,我們通過Thread.currentThread()獲取到當前線程對象, 並打印它的狀態,名稱和優先級:
public class MyThread extends Thread{
  @Override
  public void run() {
    System.out.println("線程狀態:" + Thread.currentThread().getState());
    System.out.println("線程名稱:" + Thread.currentThread().getName());
    System.out.println("線程優先級:" + Thread.currentThread().getPriority());
  }
  public static void main (String args []) {
    MyThread t = new MyThread();
    t.start();
  }
}

輸出:

線程狀態:RUNNABLE
線程名稱:Thread-0
線程優先級:5

 

 

線程的創建和啟動

創建線程主要有三種方式:
1 .繼承Thread類
2. 實現runnable接口
3. 使用Callable和Future
 
對這三種方式,創建線程的方式雖然不同,但啟動線程的方式是一樣的,都是對線程實例調用start方法來啟動線程。創建線程的時候,線程處於New狀態,只有調用了start方法后,才進入Runnable狀態
 
 

一. 繼承Thread類創建線程

可以讓當前類繼承父類Thread, 然后實例化當前類就可以創建一個線程了
public class MyThread extends Thread {
  private int i = 0;
  public void run () {
      i++;
      System.out.println(i);
  }
  public static void main (String args []){
    MyThread t = new MyThread();
    t.start();
  }
}

 

輸出
 1

 

 

二. 實現Runnable接口創建線程

也可以讓當前類繼承Runnable接口, 並將當前類實例化后得到的實例作為參數傳遞給Thread構造函數,從而創建線程
MyRunnable.java
public class MyRunnable implements Runnable {
  private int i =0;
  @Override
  public void run() {
    i++;
    System.out.println(i);
  }
}

Test.java

public class Test {
  public static void main (String args[]) {
    Thread t = new Thread(new MyRunnable());
    t.start();
  }
}

輸出

 1

 

三. 通過Callable接口和Future接口創建線程

 
Callable接口
Callable接口和Runnable接口類似, 封裝一個在線程中運行的任務,區別在於Runnable接口沒有返回值,具體來說就是通過Runnable創建的子線程不能給創建它的主線程提供返回值。而Callable接口可以讓一個運行異步任務的子線程提供返回值給創建它的主線程。 實現Callable需要重寫call方法,call方法的返回值就是你希望回傳給主線程的數據。
 
Future接口
Future可以看作是一個保存了運行線程結果信息的容器。可以和Callable接口和Runnable接口配合使用。
 
Future接口中有如下方法:
public interface Future<V> {
  V get () throws ...; // 當任務完成時, 獲取結果
  V get (long timeout, TimeUnit unit); // 在get方法的基礎上指定了超時時間
  void cancel ( boolean mayInterupt);  // 取消任務的執行
  boolean isDone (); // 任務是否已完成
  boolean isCancel ();  // 任務是否已取消
}

 

Future對於Callable和Runnable對象的作用
  • 對於Callable對象來說, Future對象可幫助它保存結果信息,當調用get方法的時候將會發生阻塞, 直到結果被返回。
  • 而對於Runnable對象來說, 無需保存結果信息, 所以get方法一般為null,  這里Future的作用主要是可以調用cancel方法取消Runnable任務
 
FutureTask
FutureTask包裝器是銜接Callable和Future的一座橋梁, 它可以將Callable轉化為一個Runnable和一個Future, 同時實現兩者的接口。 即通過
FutureTask task = new FutureTask(new Callable);

得到的task既是一個Runnable也是一個Future。這樣一來,可以先把得到的task傳入Thread構造函數中創建線程並運行(作為Runnable使用), 接着通過task.get以阻塞的方式獲得返回值(作為Future使用)

 
下面是一個示范例子:
 
MyCallable.java
import java.util.concurrent.Callable;
 
public class MyCallable implements Callable {
  @Override
  public Object call() throws Exception {
    Thread.sleep(1000);
    return "返回值";
  }
}

Test.java

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // task同時實現了Runnable接口和Future接口
    FutureTask task = new FutureTask(new MyCallable());
    // 作為 Runnable 使用
    Thread t = new Thread(task);
    t.start();
    // 作為Future使用, 調用get方法時將阻塞直到獲得返回值
    System.out.println(task.get());
  }
}

 

大約1秒后輸出:
返回值

 

 
繼承Thread和實現Runnable接口的區別
 
總體來說實現Runnable接口比繼承Thread創建線程的方式更強大和靈活,體現在以下幾個方面:
1. 可以讓多個線程處理同一個資源:實現Runnable的類的實例可以被傳入不同的Thread構造函數創建線程, 這些線程處理的是該實例里的同一個資源
2. 更強的擴展性:接口允許多繼承,而類只能單繼承, 所以實現Runnable接口擴展性更強
3. 適用於線程池:線程池中只能放入實現了Runnable或者Callable類的線程,不能放入繼承Thread的類
 
Runnable接口和Callable接口的區別
1. 實現Runnable接口要重寫run方法, 實現Callable接口要重寫call方法
2. Callable的call方法有返回值, Runnable的run方法沒有
3. call方法可以拋出異常, run方法不可以
 

四.通過線程池創建和管理線程

在實際的應用中, 通過上面三種方式直接創建線程可能會帶來一系列的問題,列舉如下:
<1>. 啟動撤銷線程性能開銷大:線程的啟動,撤銷會帶來大量開銷,大量創建/撤銷單個的生命周期很短的線程將是這一點更加嚴重
<2>. 響應速度慢:從創建線程到線程被CPU調度去執行任務需要一定時間
<3>. 線程難以統一管理
 
而使用線程池能夠解決單獨創建線程所帶來的這些問題:
對<1>: 線程池通過重用線程能減少啟動/撤銷線程帶來的性能開銷
對<2>: 相比起臨時創建線程,線程池提高了任務執行的響應速度
對<3> :  線程池能夠統一地管理線程。例如1. 控制最大並發數 2.靈活地控制活躍線程的數量
3. 實現延遲/周期執行
 
和線程池相關的接口和類
對線程池的操作, 要通過執行器(Executor)來實現。通過執行器,可以將Runnable或Callable提交(submit)到線程池中執行任務,並在線程池用完的時候關閉(shutdown)線程池。
(注意:線程池和執行器在一定程度上是等效的)
 
Executor接口
它是執行器的頂層接口, 定義了execute方法
public interface Executor {
    void execute(Runnable command);
}

 

ExecutorService接口
它是Executor接口的子接口,並對Executor接口進行了擴展,下面是部分代碼
public interface ExecutorService extends Executor {
    void shutdown();
    <T> Future<T> submit(Callable<T> task);
    <T> Future<T> submit(Runnable task, T result);
    // 其他方法
}

 

對於實現了ExecutorService接口的類的實例:
  • 調用submit方法可以將Runnable或Callable實例提交給線程池里的空閑線程執行,同時返回一個Future對象, 保存了和執行結果有關的信息
  • 當線程池用完時, 需要調用 shutdown方法關閉線程
Executors類
(注意Executor是接口,Executors是類)
Executor是一個保存着許多靜態的工廠方法的類,這些靜態方法都返回ExecutorService類型的實例
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
        }
        
     public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        }
}

 

Executors的工廠方法匯總
 

 

線程池的一般使用流程
1. 調用Executors類中的工廠方法,如newFixedThreadPool獲得線程池(執行器)實例
2. 調用submit方法提交Runnable對象或Callable對象
3. 如果提交的是Callable對象, 或者提交的是Runnable對象但想要取消,則應該保存submit方法返回的Future對象
4. 用完線程池,調用shutdown方法關閉它
 
線程池使用的例子
 
MyRunnable.java:
public class MyRunnable implements Runnable{
  @Override
  public void run() {
    for (int i=0;i<3;i++) {
      System.out.println("MyRunnable正在運行");
    }
  }
}

 

MyCallable.java:
import java.util.concurrent.Callable;
 
public class MyCallable implements Callable{
  @Override
  public Object call() throws Exception {
    for (int i=0;i<3;i++) {
      System.out.println("MyCallable正在運行");
    }
    return "回調參數";
  }
}

 

Test.java:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class Test {
  public static void main (String args []) throws ExecutionException, InterruptedException {
    // 創建一個固定數量為2的線程池
    ExecutorService service = Executors.newFixedThreadPool(2);
    // 向線程池提交Callable任務,並將結果信息保存到Future中
    Future callableFuture = service.submit(new MyCallable());
    // 向線程池提交Runnable任務,並將結果信息保存到Future中
    Future runnableFuture = service.submit(new MyRunnable());
    // 輸出結果信息
    System.out.printf("MyCallable, 完成:%b取消:%b返回值:%s%n", callableFuture.isDone(),
            callableFuture.isCancelled(), callableFuture.get());
    System.out.printf("MyRunnable, 完成:%b取消:%b返回值:%s%n", runnableFuture.isDone(),
            runnableFuture.isCancelled(), runnableFuture.get());
    // 關閉線程池
    service.shutdown();
  }
}

 

輸出:
MyCallable正在運行
MyCallable正在運行
MyCallable正在運行
MyCallable, 完成:true取消:false返回值:回調參數
MyRunnable正在運行
MyRunnable正在運行
MyRunnable正在運行
MyRunnable, 完成:false取消:false返回值:null

 

線程的運行

我們是不能通過調用線程實例的一個方法去使線程處在運行狀態的, 因為調度線程運行的工作是由CPU去完成的。通過調用線程的start方法只是使線程處在Runnable即可運行狀態, 處在Runnable狀態的線程可能在運行也可能沒有運行(就緒狀態)。
【注意】Java規范中並沒有規定Running狀態, 正在運行的線程也是處在Runnable狀態。
 

線程的阻塞(廣義)

開頭介紹線程狀態時我們說到, 線程的阻塞狀態(廣義)可進一步細分為:阻塞(Blocked), 等待(Waiting), 計時等待(Time waiting) 這三種狀態, 這里再說明一下:
  • 阻塞(Blocked)是試圖獲得對象鎖(不是java.util.concurrent庫中的鎖),而對象鎖暫時被其他線程持有導致
  • 等待(Waiting)則是調用Object.wait,Thread.join或Lock.lock等方法導致的
  • 計時等待(Time waiting)則是在等待的方法中引入了時間參數進入的狀態,例如sleep(s)
 

線程的終止

線程終止有兩個原因:
1.run方法正常運行結束, 自然終止
2.發生異常但未捕獲, 意外終止
 
這里暫不考慮第二種情況, 僅僅考慮第一種情況,則:
正如我們沒有直接的方法調用可以讓線程處在運行狀態, 我們同樣也沒有直接的方法調用可以終止一個線程(注:這里排除了已經廢棄的stop方法),所以,我們要想終止一個線程,只能是讓其“自然結束”, 即run方法體內的最后一條語句執行結束, 在這個思路的基礎上,我們有兩種方式可以結束線程:
1. 共享變量結束線程
2. 使用中斷機制結束線程
 

1. 共享變量結束線程

我們可以設置一個共享變量,在run方法體中,判斷該變量為true時則執行有效工作的代碼,判斷為false時候則退出run方法體。共享變量初始為true, 當想要結束線程的時候將共享變量置為false就可以了
 
優點: 簡單易懂,在非阻塞的情況下能正常工作
缺點:  當線程阻塞的時候, 將不會檢測共享變量,線程可能不能及時地退出。
 
 
public class InteruptSimulation implements Runnable{
  private volatile static boolean stop = false;
  @Override
  public void run() {
    try {
      while (!stop) {
        System.out.println("線程正在運行");
        // 休眠5秒
        Thread.sleep(5000);
      }
      System.out.println("線程終止");
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
 
  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptSimulation());
    t.start();
    // 休眠1秒
    Thread.sleep(1000);
    // 將共享變量stop置為true
System.out.println("發出終止線程的信號");
    stop = true;   } }

 

 

線程正在運行
發出終止線程的信號
// 約5s后輸出
線程終止

 

 
如上所示, 我們試圖在線程啟動1秒后就結束線程,但實際上在大約5秒后線程才結束。這是因為線程啟動后因為休眠(sleep)而陷入了阻塞狀態(等待),這時候自然是不會檢測stop變量了。 所以在阻塞狀態下,共享變量結束線程的方式可能並不具備良好的時效性
 

2. 利用中斷機制結束線程

 
因為直接使用共享變量的方式不能很好應對線程阻塞的情況,所以我們一般采用中斷機制結束線程,單從形式上看,采用中斷機制結束線程和共享變量的管理方式
並沒有太大區別,假設t是當前線程,則調用t.interrupt()會將線程中的中斷狀態位置為true, 然后通過t.isInterrupted()可以返回中斷狀態位的值。
區別在於:當剛好遇到線程阻塞的時候, 中斷會喚醒阻塞線程,這樣的話我們就可以及時的結束線程了。
 
 
public class InteruptReal implements Runnable{
  @Override
  public void run() {
    try {
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("線程正在運行");
        Thread.sleep(5000);
      }
    } catch (InterruptedException e) {
      // 發生中斷異常后,中斷狀態位會被置為false,這里不做任何操作
    }
    System.out.println("線程已中斷");
  }

  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptReal());
    t.start();
    // 休眠1s
    Thread.sleep(1000);
    System.out.println("發出終止線程的信號");
    t.interrupt();
  }
}

 

輸出:

線程正在運行
發出終止線程的信號
// 立即輸出
線程已中斷

 

線程現在已經能夠及時退出啦

 

中斷線程的時候, 如果線程處在阻塞狀態,則會1. 喚醒阻塞線程,使其重新重新處於RUNNABLE狀態 2. 將中斷狀態位 置為false
 
注意! 在喚醒阻塞線程的同時會將中斷狀態位置為false, 這也許讓人感覺有些奇怪,但這說明了JAVA給了你更多的處理線程的自由度。在被阻塞的線程喚醒后,你可以選擇再次發起中斷,也可以選擇不中斷。
 
例子如下: 喚醒阻塞線程后, 中斷狀態位會被置為false
public class InteruptReal implements Runnable{
  @Override
  public void run() {
    try {
      while (!Thread.currentThread().isInterrupted()) {
        System.out.println("線程正在運行");
        Thread.sleep(5000);
      }
    } catch (InterruptedException e) {
      System.out.println("中斷狀態位:"+Thread.currentThread().isInterrupted());
    }
   
  }

  public static void main (String args []) throws InterruptedException {
    Thread t = new Thread(new InteruptReal());
    t.start();
    // 休眠1s
    Thread.sleep(1000);
    System.out.println("發出中斷");
    t.interrupt();
  }
}

輸出:

線程正在運行
發出中斷
中斷狀態位:false

 

 
 
【注意】 Java已經廢棄了線程的stop方法, 因為在多線程中,它極有可能破壞預期的原子操作, 並因此使得線程共享變量取得錯誤的值。
 

線程的常用方法調用

Thread.sleep

讓線程休眠一段時間,調用它時,線程將進入計時等待狀態(Time waiting)

Thread.yeild

使正在運行的線程讓出CPU的執行權,進入就緒狀態(注意是就緒),將占用CPU的機會讓給優先級相同或者更高的線程

Thread.join

join方法的作用是等待某個線程終止后才繼續執行,效果類似於Future的get方法。 例如我們可能存在這樣一個需求: 在主線程中啟動了一個子線程,但希望子線程運行完后才執行主線程中的代碼,在子線程運行完畢前主線程處於阻塞的狀,這時就可以使用join方法
 
舉個例子,在下面我們想要在子線程t執行完畢后,在主線程中輸出“子線程執行完畢”, 下面的使用顯然不能達到效果, 因為主線程的輸出並不會因為子線程的運行而阻塞。
public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在執行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    System.out.println("子線程執行完畢");
  }
}

 

輸出:
子線程執行完畢
Thread-0正在執行
Thread-0正在執行
Thread-0正在執行

 

 
而使用join方法后就可以達到我們想要的效果
public class JoinRunnable implements Runnable{
  @Override
  public void run() {
    for(int i=0;i<3;i++) {
      System.out.println(Thread.currentThread().getName()+ "正在執行");
    }
  }
 
  public static void main (String args[]) throws InterruptedException {
    Thread t = new Thread(new JoinRunnable());
    t.start();
    t.join();
    System.out.println("子線程執行完畢");
  }
}

輸出:

Thread-0正在執行
Thread-0正在執行
Thread-0正在執行
子線程執行完畢

 

【注意】 join方法可以用Future的實現代替


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM