java基礎鞏固,中級篇(多線程)

本博客總結網絡其他文章和自己的理解,僅用於自己鞏固基礎跟分享給大家參考而已

java多線程的創建和啓動

多線程即在同一時間,可以做多件事情。 

創建多線程有幾種方式,分別是

1.繼承線程類 (重寫run方法,並調用start方法啓動,繼承Thread類,通過重寫run()方法定義了一個新的線程類,其中run()方法

的方法體代表了線程需要完成的任務,稱之爲線程執行體。當創建此線程類對象時一個新的線程得以創建,並進入到線程新建狀

態。通過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不一定會馬上得以執行,這取決於CPU調

度時機。)

2.實現Runnable接口 (實現Runnable接口,並重寫該接口的run()方法,該run()方法同樣是線程執行體,創建Runnable實現類的

實例,並以此實例作爲Thread類的target來創建Thread對象,該Thread對象纔是真正的線程對象)

3.匿名類

4.實現Callable接口(可以拿到返回結果)

注: 啓動線程是start()方法,run()並不能啓動一個新的線程

 

Thread和Runnable之間的關係

先來看一段代碼

public class ThreadTest {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                Runnable myRunnable = new MyRunnable();
                Thread thread = new MyThread(myRunnable);
                thread.start();
            }
        }
    }
}


class MyThread extends Thread {

    private int i = 0;
    
    public MyThread(Runnable runnable){
        super(runnable);
    }

    @Override
    public void run() {
        System.out.println("in MyThread run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

class MyRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        System.out.println("in MyRunnable run");
        for (i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

}

同樣的,與實現Runnable接口創建線程方式相似,不同的地方在於下面這段代碼

 Thread thread = new MyThread(myRunnable);

那麼這種方式可以順利創建出一個新的線程麼?答案是肯定的。至於此時的線程執行體到底是MyRunnable接口中的run()方法還

是MyThread類中的run()方法呢?通過輸出我們知道線程執行體是MyThread類中的run()方法。其實原因很簡單,因爲Thread類本

身也是實現了Runnable接口,而run()方法最先是在Runnable接口中定義的方法。

當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable接口

並重寫了run()方法的類中的run()方法。但是上述給到的列子中,由於多態的存在,根本就沒有執行到Thread類中的run()方法,

而是直接先執行了運行時類型即MyThread類中的run()方法。

總結一下:Thread是線程,Runnable是線程執行體。Runnable接口真的是僅僅是一個接口,它並不是一個線程,不能用來啓動

線程,真正執行線程的還是Thread。Runnable字面意思也就是可執行的,我的理解是Runnable接口用來定義可以給線程執行的

東西,也就是把run()方法的代碼仍在線程去執行。

java線程中的幾種狀態

新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此

線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就     緒

狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到

其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

1.等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

2.同步阻塞 -- 線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

3.其他阻塞 -- 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終

止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

java線程的生命週期及管理

如圖所示線程的生命週期(圖片也來源於網絡)

線程的管理有幾點如下

1、線程睡眠——sleep

      如果我們需要讓當前正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過調用Thread的sleep方法。

   (1)sleep是靜態方法,最好不要用Thread的實例對象調用它,因爲它睡眠的始終是當前正在運行的線程,而不是調用它的線

程對象,它只對正在運行狀態的線程對象有效。

 (2)Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提高程序的執行效率。但是不管程序員

怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能做到精準控制。因爲使用sleep方法之後,線程是進入阻塞狀態的,

只有當睡眠的時間結束,纔會重新進入到就緒狀態,而就緒狀態進入到運行狀態,是由系統控制的,我們不可能精準的去幹涉

它,所以如果調用Thread.sleep(1000)使得線程睡眠1秒,可能結果會大於1秒。

2、線程讓步——yield

      yield()方法和sleep()方法有點相似,它也是Thread類提供的一個靜態的方法,它也可以讓當前正在執行的線程暫停,讓出cpu

資源給其他的線程。但是和sleep()方法不同的是,它不會進入到阻塞狀態,而是進入到就緒狀態。yield()方法只是讓當前線程暫

停一下,重新進入就緒的線程池中,讓系統的線程調度器重新調度器重新調度一次,完全可能出現這樣的情況:當某個線程調用

yield()方法之後,線程調度器又將其調度出來重新進入到運行狀態執行。

實際上,當某個線程調用了yield()方法暫停之後,優先級與當前線程相同,或者優先級比當前線程更高的就緒狀態的線程更有可

能獲得執行的機會,當然,只是有可能,因爲我們不可能精確的干涉cpu調度線程

關於sleep()方法和yield()方的區別如下:

①、sleep方法暫停當前線程後,會進入阻塞狀態,只有當睡眠時間到了,纔會轉入就緒狀態。而yield方法調用後 ,是直接進入

就緒狀態,所以有可能剛進入就緒狀態,又被調度到運行狀態。

②、sleep方法聲明拋出了InterruptedException,所以調用sleep方法的時候要捕獲該異常,或者顯示聲明拋出該異常。而yield方

法則沒有聲明拋出任務異常。

③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法來控制併發線程的執行。

3、線程合併——join

線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程執行,應用場景是當一個線程必須等待另一個線程執行完畢才能

執行時,Thread類提供了join方法來完成這個功能,注意,它不是靜態方法。

從上面的方法的列表可以看到,它有3個重載的方法

void join()      
     當前線程等該加入該線程後面,等待該線程終止。    
void join(long millis)  
     當前線程等待該線程終止的時間最長爲 millis 毫秒。 如果在millis時間內,
該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度  
void join(long millis,int nanos)   
     等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。如果在millis時間內,
該線程沒有執行完,那麼當前線程進入就緒狀態,重新等待cpu調度  

4、設置線程的優先級

     每個線程執行時都有一個優先級的屬性,優先級高的線程可以獲得較多的執行機會,而優先級低的線程則獲得較少的執行機

會。與線程休眠類似,線程的優先級仍然無法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的概率較大,優先級

低的也並非沒機會執行。

每個線程默認的優先級都與創建它的父線程具有相同的優先級,在默認情況下,main線程具有普通優先級。

Thread類提供了setPriority(int newPriority)和getPriority()方法來設置和返回一個指定線程的優先級,其中setPriority方法的參

數是一個整數,範圍是1~·0之間,也可以使用Thread類提供的三個靜態常量:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5

 雖然Java提供了10個優先級別,但這些優先級別需要操作系統的支持。不同的操作系統的優先級並不相同,而且也不能很好的

和Java的10個優先級別對應。所以我們應該使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三個靜態常量來設定優

先級,這樣才能保證程序最好的可移植性。

5、後臺(守護)線程

  守護線程使用的情況較少,但並非無用,舉例來說,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應

用時候,使用的數據庫連接池,連接池本身也包含着很多後臺線程,監控連接個數、超時時間、狀態等等。調用線程對象的方法

setDaemon(true),則可以將其設置爲守護線程。守護線程的用途爲:

 守護線程通常用於一些後臺作業,例如在你的應用程序運行時播放背景音樂,在編輯器裏做自動語法檢查、自動保存等功能。

 Java的垃圾回收也是一個守護線程。守護線的好處就是你不需要關心它的結束問題。例如你在你的應用程序運行的時候希望播放

背景音樂,如果將這個播放背景音樂的線程設定爲非守護線程,那麼在用戶請求退出的時候,不僅要退出主線程,還要通知播放

背景音樂的線程退出;如果設定爲守護線程則不需要了。

setDaemon方法的詳細說明:

public final void setDaemon(boolean on)        將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java 虛擬機退出。    
         該方法必須在啓動線程前調用。 該方法首先調用該線程的 checkAccess 方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。   
  參數:
     on - 如果爲 true,則將該線程標記爲守護線程。    
  拋出:    
    IllegalThreadStateException - 如果該線程處於活動狀態。    
    SecurityException - 如果當前線程無法修改該線程。

 JRE判斷程序是否執行結束的標準是所有的前臺執線程行完畢了,而不管後臺線程的狀態,因此,在使用後臺縣城時候一定要注

意這個問題

6、正確結束線程

Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit這些終止線程運行的方法已經被廢棄了,使用

它們是極端不安全的!想要安全有效的結束一個線程,可以使用下面的方法:

 正常執行完run方法,然後結束掉;

控制循環條件和判斷條件的標識符來結束掉線程。

線程同步

 java允許多線程併發控制,當多個線程同時操作一個可共享的資源變量時(如數據的增刪改查),將會導致數據不準確,相互之

間產生衝突,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調用,從而保證了該變量的唯一性和準確性。

1、同步方法     

      即有synchronized關鍵字修飾的方法。由於java的每個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方

法。在調用該方法前,需要獲得內置鎖,否則就處於阻塞狀態。

public synchronized void save(){}

 synchronized關鍵字也可以修飾靜態方法,此時如果調用該靜態方法,將會鎖住整個類。

2、同步代碼塊     

 即有synchronized關鍵字修飾的語句塊。被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。

同步是一種高開銷的操作,因此應該儘量減少同步的內容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關鍵代碼

即可。

3、使用特殊域變量(volatile)實現線程同步      

   volatile關鍵字爲域變量的訪問提供了一種免鎖機制;

   使用volatile修飾域相當於告訴虛擬機該域可能會被其他線程更新;

   因此每次使用該域就要重新計算,而不是使用寄存器中的值;

   volatile不會提供任何原子操作,它也不能用來修飾final類型的變量。

多線程中的非同步問題主要出現在對域的讀寫上,如果讓域自身避免這個問題,則就不需要修改操作該域的方法。用final域,有

鎖保護的域和volatile域可以避免非同步的問題。

4、使用重入鎖(Lock)實現線程同步

 在javaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使

用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力。ReenreantLock類的常用方法有以下幾個

 ReentrantLock() : 創建一個ReentrantLock實例         
 lock() : 獲得鎖        
 unlock() : 釋放鎖

 ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用。

線程的通信

1、藉助於Object類的wait()、notify()和notifyAll()實現通信

     線程執行wait()後,就放棄了運行資格,處於凍結狀態;

     線程運行時,內存中會建立一個線程池,凍結狀態的線程都存在於線程池中,notify()執行時喚醒的也是線程池中的線程,線

程池中有多個線程時喚醒第一個被凍結的線程。

 notifyall(), 喚醒線程池中所有線程。


注: (1) wait(), notify(),notifyall()都用在同步裏面,因爲這3個函數是對持有鎖的線程進行操作,而只有同步纔有鎖,所以要使

用在同步中;
      

(2) wait(),notify(),notifyall(),  在使用時必須標識它們所操作的線程持有的鎖,因爲等待和喚醒必須是同一鎖下的線程;而鎖可

以是任意對象,所以這3個方法都是Object類中的方法。

2、使用Condition控制線程通信

      jdk1.5中,提供了多線程的升級解決方案爲:

     (1)將同步synchronized替換爲顯式的Lock操作;

     (2)將Object類中的wait(), notify(),notifyAll()替換成了Condition對象,該對象可以通過Lock鎖對象獲取;

     (3)一個Lock對象上可以綁定多個Condition對象,這樣實現了本方線程只喚醒對方線程,而jdk1.5之前,一個同步只能有一

個鎖,不同的同步只能用鎖來區分,且鎖嵌套時容易死鎖。

3、使用阻塞隊列(BlockingQueue)控制線程通信

       BlockingQueue是一個接口,也是Queue的子接口。BlockingQueue具有一個特徵:當生產者線程試圖向BlockingQueue中放入元素時,如果該隊列已滿,則線程被阻塞;但消費者線程試圖從BlockingQueue中取出元素時,如果隊列已空,則該線程阻塞。程序的兩個線程通過交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。

BlockingQueue提供如下兩個支持阻塞的方法:

  (1)put(E e):嘗試把Eu元素放如BlockingQueue中,如果該隊列的元素已滿,則阻塞該線程。

  (2)take():嘗試從BlockingQueue的頭部取出元素,如果該隊列的元素已空,則阻塞該線程。

BlockingQueue繼承了Queue接口,當然也可以使用Queue接口中的方法,這些方法歸納起來可以分爲如下三組:

  (1)在隊列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,當該隊列已滿時,這三個方法分別會拋出異

常、返回false、阻塞隊列。

  (2)在隊列頭部刪除並返回刪除的元素。包括remove()、poll()、和take()方法,當該隊列已空時,這三個方法分別會

拋出異常、返回false、阻塞隊列。

  (3)在隊列頭部取出但不刪除元素。包括element()和peek()方法,當隊列已空時,這兩個方法分別拋出異常、返回

false。

BlockingQueue接口包含如下5個實現類:

ArrayBlockingQueue :基於數組實現的BlockingQueue隊列。

LinkedBlockingQueue:基於鏈表實現的BlockingQueue隊列。

PriorityBlockingQueue:它並不是保準的阻塞隊列,該隊列調用remove()、poll()、take()等方法提取出元素時,並不是取出隊列中存在時間最長的元素,而是隊列中最小的元素。
                       它判斷元素的大小即可根據元素(實現Comparable接口)的本身大小來自然排序,也可使用Comparator進行定製排序。

SynchronousQueue:同步隊列。對該隊列的存、取操作必須交替進行。

DelayQueue:它是一個特殊的BlockingQueue,底層基於PriorityBlockingQueue實現,不過,DelayQueue要求集合元素都實現Delay接口(該接口裏只有一個long getDelay()方法),
            DelayQueue根據集合元素的getDalay()方法的返回值進行排序。

 線程池

 合理利用線程池能夠帶來三個好處。

1.降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。

2.提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。

3.提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,會降低系統的穩定性,使用線程池可以進

行統一的分配,調優和監控。

1、使用Executors工廠類產生線程池

Executor線程池框架的最大優點是把任務的提交和執行解耦。客戶端將要執行的任務封裝成Task,然後提交即可。而Task如

何執行客戶端則是透明的。具體點講,提交一個Callable對象給ExecutorService(如最常用的線程池ThreadPoolExecutor),將

得到一個Future對象,調用Future對象的get方法等待執行結果。線程池實現原理類結構圖如下:

除此之外,ExecutorService還繼承了Executor接口(注意區分Executor接口和Executors工廠類),這個接口只有一個execute()

方法,最後我們看一下整個繼承樹:

   使用Executors執行多線程任務的步驟如下:

   調用Executors類的靜態工廠方法創建一個ExecutorService對象,該對象代表一個線程池;

   創建Runnable實現類或Callable實現類的實例,作爲線程執行任務;

   調用ExecutorService對象的submit()方法來提交Runnable實例或Callable實例;

   當不想提交任務時,調用ExecutorService對象的shutdown()方法來關閉線程池。

使用Executors的靜態工廠類創建線程池的方法如下:

1、newFixedThreadPool() : 

作用:該方法返回一個固定線程數量的線程池,該線程池中的線程數量始終不變,即不會再創建新的線程,也不會銷燬已經創

建好的線程,自始自終都是那幾個固定的線程在工作,所以該線程池可以控制線程的最大併發數。 

栗子:假如有一個新任務提交時,線程池中如果有空閒的線程則立即使用空閒線程來處理任務,如果沒有,則會把這個新任務存

在一個任務隊列中,一旦有線程空閒了,則按FIFO方式處理任務隊列中的任務。

2、newCachedThreadPool() : 

     作用:該方法返回一個可以根據實際情況調整線程池中線程的數量的線程池。即該線程池中的線程數量不確定,是根據實際情

況動態調整的。 

栗子:假如該線程池中的所有線程都正在工作,而此時有新任務提交,那麼將會創建新的線程去處理該任務,而此時假如之前有

一些線程完成了任務,現在又有新任務提交,那麼將不會創建新線程去處理,而是複用空閒的線程去處理新任務。那麼此時有人

有疑問了,那這樣來說該線程池的線程豈不是會越集越多?其實並不會,因爲線程池中的線程都有一個“保持活動時間”的參數,

通過配置它,如果線程池中的空閒線程的空閒時間超過該“保存活動時間”則立刻停止該線程,而該線程池默認的“保持活動時間”爲

60s。

3、newSingleThreadExecutor() : 

     作用:該方法返回一個只有一個線程的線程池,即每次只能執行一個線程任務,多餘的任務會保存到一個任務隊列中,等待這

一個線程空閒,當這個線程空閒了再按FIFO方式順序執行任務隊列中的任務。

4、newScheduledThreadPool() : 

     作用:該方法返回一個可以控制線程池內線程定時或週期性執行某任務的線程池。

5、newSingleThreadScheduledExecutor() : 

     作用:該方法返回一個可以控制線程池內線程定時或週期性執行某任務的線程池。只不過和上面的區別是該線程池大小爲1,

而上面的可以指定線程池的大小。

注:Executors只是一個工廠類,它所有的方法返回的都是ThreadPoolExecutorScheduledThreadPoolExecutor這兩個類的實例。

 ExecutorService有如下幾個執行方法:

execute(Runnable)   這個方法接收一個Runnable實例,並且異步的執行
submit(Runnable)    submit(Runnable)和execute(Runnable)區別是前者可以返回一個Future對象,通過                    返回的Future對象,我們可以檢查提交的任務是否執行完畢,如果任務執行完成,future.get()方法會返回一個null。注意,future.get()方法會產生阻塞
submit(Callable)     submit(Callable)和submit(Runnable)類似,也會返回一個Future對象,但是除此之外,submit(Callable)接收的是一個Callable的實現,Callable接口中的call()方法有一個返回值,可以返回任務的執行結果,而Runnable接口中的run()方法是void的,沒有返回值
invokeAny(...)       invokeAny(...) 方法接收的是一個Callable的集合,執行這個方法不會返回Future,但是會返回所有Callable任務中其中一個任務的執行結果。這個方法也無法保證返回的是哪個任務的執行結果,反正是其中的某一個
invokeAll(...)        invokeAll(...)與 invokeAny(...)類似也是接收一個Callable集合,但是前者執行之後會返回一個Future的List,其中對應着每個Callable任務執行後的Future對象

 

ExecutorService關閉方法

   當我們使用完成ExecutorService之後應該關閉它,否則它裏面的線程會一直處於運行狀態。舉個例子,如果的應用程序是通過

main()方法啓動的,在這個main()退出之後,如果應用程序中的ExecutorService沒有關閉,這個應用將一直運行。之所以會出現

這種情況,是因爲ExecutorService中運行的線程會阻止JVM關閉。

要關閉ExecutorService中執行的線程,我們可以調用ExecutorService.shutdown()方法。在調用shutdown()方法之後,

ExecutorService不會立即關閉,但是它不再接收新的任務,直到當前所有線程執行完成纔會關閉,所有在shutdown()執行之前提

交的任務都會被執行。

如果想立即關閉ExecutorService,我們可以調用ExecutorService.shutdownNow()方法。這個動作將跳過所有正在執行的任

務和被提交還沒有執行的任務。但是它並不對正在執行的任務做任何保證,有可能它們都會停止,也有可能執行完成。

使用Java8增強的ForkJoinPool產生線程池

      在Java 8中,引入了自動並行化的概念。它能夠讓一部分Java代碼自動地以並行的方式執行,前提是使用了ForkJoinPool。

     ForkJoinPool同ThreadPoolExecutor一樣,也實現了Executor和ExecutorService接口。它使用了一個無限隊列來保存需要執

行的任務,而線程的數量則是通過構造函數傳入,如果沒有向構造函數中傳入希望的線程數量,那麼當前計算機可用的CPU數量

會被設置爲線程數量作爲默認值。

      ForkJoinPool主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題。典型的應用比如快速排序算法。這裏的要點

在於,ForkJoinPool需要使用相對少的線程來處理大量的任務。比如要對1000萬個數據進行排序,那麼會將這個任務分割成兩個

500萬的排序任務和一個針對這兩組500萬數據的合併任務。以此類推,對於500萬的數據也會做出同樣的分割處理,到最後會設

置一個閾值來規定當數據規模到多少時,停止這樣的分割處理。比如,當元素的數量小於10時,會停止分割,轉而使用插入排序

對它們進行排序。那麼到最後,所有的任務加起來會有大概2000000+個。問題的關鍵在於,對於一個任務而言,只有當它所有

的子任務完成之後,它才能夠被執行。所以當使用ThreadPoolExecutor時,使用分治法會存在問題,因爲ThreadPoolExecutor中

的線程無法像任務隊列中再添加一個任務並且在等待該任務完成之後再繼續執行。而使用ForkJoinPool時,就能夠讓其中的線程

創建新的任務,並掛起當前的任務,此時線程就能夠從隊列中選擇子任務執行。比如,我們需要統計一個double數組中小於0.5的

元素的個數。

死鎖

死鎖的四個必要條件

 互斥條件:資源不能被共享,只能被同一個進程使用

請求與保持條件:已經得到資源的進程可以申請新的資源

非剝奪條件:已經分配的資源不能從相應的進程中被強制剝奪

循環等待條件:系統中若干進程組成環路,該環路中每個進程都在等待相鄰進程佔用的資源。

舉個常見的死鎖例子:進程A中包含資源A,進程B中包含資源B,A的下一步需要資源B,B的下一步需要資源A,所以它們就互相

等待對方佔有的資源釋放,所以也就產生了一個循環等待死鎖。

處理死鎖的方法

忽略該問題,也即鴕鳥算法。當發生了什麼問題時,不管他,直接跳過,無視它;

檢測死鎖並恢復;

資源進行動態分配;

破除上面的四種死鎖條件之一。

 

Callable介紹

Callable接口代表一段可以調用並返回結果的代碼;Future接口表示異步任務,是還沒有完成的任務給出的未來結果。所以說Callable

用於產生結果,Future用於獲取結果。

Callable接口使用泛型去定義它的返回類型。Executors類提供了一些有用的方法在線程池中執行Callable內的任務。由於Callable任

務是並行的(並行就是整體看上去是並行的,其實在某個時間點只有一個線程在執行),我們必須等待它返回的結果。 

java.util.concurrent.Future對象爲我們解決了這個問題。在線程池提交Callable任務後返回了一個Future對象,使用它可以知道

Callable任務的狀態和得到Callable返回的執行結果。Future提供了get()方法讓我們可以等待Callable結束並獲取它的執行結果。

Callable位於java.util.concurrent包下,它也是一個接口,在它裏面也只聲明瞭一個方法,只不過這個方法叫做call():

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

 可以看到,這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。

使用Callable

一般情況下是配合ExecutorService來使用的,在ExecutorService接口中聲明瞭若干個submit方法的重載版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

第一個submit方法裏面的參數類型就是Callable。

暫時只需要知道Callable一般是和ExecutorService配合來使用的,具體的使用方法講在後面講述。

一般情況下我們使用第一個submit方法和第三個submit方法,第二個submit方法很少使用。

Future接口

Future是一個接口,代表了一個異步計算的結果。接口中的方法用來檢查計算是否完成、等待完成和得到計算的結果。當計算完

成後,只能通過get()方法得到結果,get方法會阻塞直到結果準備好了。如果想取消,那麼調用cancel()方法。其他方法用於確定

任務是正常完成還是取消了。一旦計算完成了,那麼這個計算就不能被取消。

FutureTask類

FutureTask類實現了RunnableFuture接口,而RunnnableFuture接口繼承了Runnable和Future接口,所以說FutureTask是一個提

供異步計算的結果的任務。

FutureTask可以用來包裝Callable或者Runnbale對象。因爲FutureTask實現了Runnable接口,所以FutureTask也可以被提交給

Executor(如上面例子那樣)。

FutureTask的狀態

FutureTask中有一個表示任務狀態的int值,初始爲NEW。定義如下:

 private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;



可能的狀態轉換包括:

NEW -> COMPLETING -> NORMAL
NEW -> COMPLETING -> EXCEPTIONAL
NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED


構造方法

FutureTask一共有兩個構造方法,如下:

 public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
    
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }


第一個構造方法好理解;第二個方法是將Runnbale和結果組合成一個Callable,這個可以通過Excutors.callable()方法得出結論:

 public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }
    
static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }


從上面可以看到RunnableAdapter實現了Callable並且在call方法中調用了Runnable的run方法,然後將結果返回,這其實就是一

個適配器模式啊。

所以說兩個構造方法最終都是得到了一個Callable以及設置了初始狀態爲NEW。

run方法

當將FutureTask提交給Executor後,Executor執行FutureTask時會執行其run方法,下面看一下run方法中做了哪些事情。

public void run() {
        //如果狀態不爲NEW或者CAS當前執行線程失敗,直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        //嘗試調用Callable.call
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    //出現異常了,調用setException方法
                    result = null;
                    ran = false;
                    setException(ex);
                }
                //如果成功了,調用set方法
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            //如果在執行過程,任務被取消了
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

從上面可以看到,任務可以被執行的前提是當前狀態爲NEW以及CAS當前執行線程成功,也就是runner值,代表執行Callable的

線程。從這個看到run方法就是調用Callable的call方法,然後如果出現異常了就調用setException方法,如果成功執行了,那麼調

用set方法,下面我們分別來看這幾種情況。

set方法

當Callable成功執行後,會調用set方法將結果傳出。源碼如下:

protected void set(V v) {
        //完成NEW->COMPLETING->NORMAL狀態轉換
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }



從上面可以看到,將outcome變量賦值爲結果,並將state狀態更新,最後調用finishCompletion()方法。finishCompletion()方法將

移除和通知所有等待線程,這個方法後面再說。下面先看setException方法。

setException方法

setException方法如下:

//完成NEW->COMPLETING->EXCEPTIONAL狀態轉換
protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }



從上面看到,該方法和set方法類似,完成狀態轉換,將結果設置爲Throwable並調用finishCompletion通知和移除等待線程。

get方法

當想得到FutureTask的結算結果時,調用get方法,get方法可以允許多個線程調用,下面的例子展示了多個線程調用get的情況。

 public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("Start:" + System.nanoTime());
        FutureTask<Long> futureTask = new FutureTask<Long>(new SumTask());
        Executor executor=Executors.newSingleThreadExecutor();
        executor.execute(futureTask);
        for(int i=0;i<5;i++){
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("get result "+futureTask.get());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        System.out.println(futureTask.get());
        System.out.println("End:" + System.nanoTime());
    }



該例子展示了一共有5個線程想得到FutureTask的結果,一旦調用get,那麼該線程就會阻塞。

FutureTask的get方法實現如下:

 public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }



從上面的代碼可以看到,如果當前任務的狀態不大於COMPLETING,那麼會調用awaitDone方法,這個方法會將調用的線程掛

起;否則直接調用report方法返回結果。

在前面set和setException方法中可以得出結論:當狀態從NEW變爲COMPLETING後,纔會將outcome賦值,也就是狀態是NEW

或者COMPLETING時,outcome都還未賦值,也就意味着計算仍在進行,那麼此時想要get到結果,就必須等待。下面先看下

awaitDone方法是如何將調用線程阻塞的。awaitDone的兩個參數分別表示是否定時,以及定時的時間多少。get的另一個重載方

法就提供了超時限制。awaitDone方法如下:

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            //如果當前線程被中斷了,移除並拋出異常
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //如果狀態大於COMPLETING,說明已經計算已經完成了
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //狀態是COMPLETING,在set和setException方法中可以看到處於該狀態馬上就會進入下一個狀態
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //新建一個等待節點
            else if (q == null)
                q = new WaitNode();
            //還沒有入隊,嘗試入隊
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //如果限制了時間
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //掛起指定時間
                LockSupport.parkNanos(this, nanos);
            }
            //無限掛起
            else
                LockSupport.park(this);
        }
    }



上面的代碼中有一個WaitNode類,該類表示等待節點,保存等待的線程以及下一個節點,是一個單鏈表結構,其定義如下:

 static final class WaitNode {
        volatile Thread thread;
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }



awaitDone方法中進入死循環後,主要有幾步:

如果線程被中斷了,移除節點,拋出異常

如果狀態大於COMPLETING,那麼直接返回

如果狀態是COMPLETING,在set和setException可以看到,處於COMPLETING是一個暫時狀態,很快就會進入下一個狀態,所

以這兒就調用了Thread.yield()方法讓步一下

如果狀態是NEW且節點爲null,那麼創建一個節點

如果還沒有將當前線程加入隊列,那麼將當前線程加入到等待隊列中。由於WaitNode是一個單鏈表結構,FutureTask中保存了

waiters的變量,就可以沿着該變量得到所有等待的線程

如果限制了時間,那麼計算出生出超出時間,掛起指定時間。當解除掛起時,如果計算還未完成,那麼將會由於沒有時間了,調

用removeWaiter方法移除節點。

如果沒有限制時間,那麼將線程無限掛起

上面幾種情況下,都涉及了移除節點,removeWaiter方法就是刪除單鏈表中一個節點的實現。

當線程被解除掛起,或計算已經完成後,將會get方法中將會調用report返回結果,其實現如下:

 private V report(int s) throws ExecutionException {
        Object x = outcome;
        //如果計算正常結束
        if (s == NORMAL)
            return (V)x;
        //如果計算被取消了
        if (s >= CANCELLED)
            throw new CancellationException();
        //如果計算以異常計算
        throw new ExecutionException((Throwable)x);
    }



從上面可以看到report會根據任務的狀態不同返回不同的結果。

如果計算正常結束,即狀態是NORMAL,那麼返回正確的計算結果

如果計算被取消了,即狀態大於等於CANCELLED,那麼拋出CancellationException

如果計算以異常結束,即狀態是EXCEPTIONAL,那麼拋出ExecutionException

finishCompletion方法

在set方法和setException方法中,當將結果賦值後,都調用了finishCompletion方法來移除和通知等待線程。由於get方法中可以

掛起了一羣等待節點,那麼當結果被計算出來了,自然應該通知那些等待線程。finishCompletion的實現如下:

 private void finishCompletion() {
        //如果有等待線程,從頭開始解除掛起
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    //得到等待節點的線程,解除掛起
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }
        
        done();

        callable = null;        // to reduce footprint
    }



finishCompletion的實現比較簡單,就是遍歷等待線程的單鏈表,釋放那些等待線程。當線程被釋放後,那麼在awaitDone的死循

環中就會進入下一個循環,由於狀態已經變成了NORMAL或者EXCEPTIONAL,將會直接跳出循環。

釋放了所有線程後,將會調用done()方法,FutureTask的done()方法默認沒有任何實現,子類可以在該方法中調用完成回調以及

記錄操作等等。

上面的方法分析完了FutureTask的主要流程,包括調用get線程的阻塞、run方法執行、計算結果的返回。下面再來看一些取消、

查看狀態的方法。

cancel方法

cancel方法用於取消Callable的計算。參數mayInterruptIfRunning指明是否應該中斷正在運行的任務,返回值表示取消是否成功

了。其源碼如下:

 public boolean cancel(boolean mayInterruptIfRunning) {
        if (!(state == NEW &&
              UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
                  mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
            return false;
        try { 
            //如果需要中斷
            if (mayInterruptIfRunning) {
                try {
                    Thread t = runner;
                    if (t != null)
                        t.interrupt();
                } finally {
                    //最終狀態INTERRUPTED
                    UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                }
            }
        } finally {
            //釋放等待線程
            finishCompletion();
        }
        return true;
    }



從上面可以看到如果是需要中斷正在執行的任務,那麼狀態轉換將會是NEW->INTERRPUTING->INTERRUPTED;如果不需要

中斷正在執行的任務,那麼狀態轉換將會是NEW->CANCELD。不管是否中斷,最終都會調用finishCompletion()完成對等待線程

的釋放。

當這些線程釋放後,再進入到awaitDone中的循環時,返回的狀態將會是大於等於CANCELD,在report方法中將會得到

CancellationException異常。

isDone方法

Future接口中isDone方法表明任務是否已經完成了,如果完成了,那麼返回true,否則false。下面是FutureTask的實現:

 public boolean isDone() {
        return state != NEW;
    }


可以看到只要狀態從初始狀態NEW完成了一次轉換,那麼就說明任務已經被完成了。
 

 

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