【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();   } }

輸出:java

線程狀態: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.javaide

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(); } }

輸出:spa

線程正在運行 發出中斷 中斷狀態位: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的實現代替
相關文章
相關標籤/搜索