Java併發編程-Executor框架與線程池

 

線程簡介

        併發編程是Java程序員最重要的技能之一,也是最難掌握的一種技能。它要求編程者對計算機最底層的運作原理有深刻的理解,同時要求編程者邏輯清晰、思維縝密,這樣才能寫出高效、安全、可靠的多線程併發程序。現代操作系統在運行一個程序時,會爲其創建一個進程。例如,啓動一個Java程序,操作系統就會創建一個Java進程。線程是現代操作系統調度的最小單元,也叫輕量級進程,在一個進程裏可以創建多個線程,這些線程都擁有各自的計算器、堆棧和局部變量等屬性,並且能夠訪問共享的內存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執行。本文只是先對Executor進行淺析,對java併發編程基礎做一個較爲全面的解析。

基本概念

  程序:程序是由序列組成的,告訴計算機如何完成一個具體的任務

  進程:進程是一個程序的實例,是併發執行的程序在執行過程中分配和管理資源的基本單位,每一個進程都有它自己的地址空    間,一般情況下,包括文本區域(text region)、數據區(data region)和堆棧(stack region)。

  線程:線程自己不擁有系統資源,但它可與同屬一個 進程的其它線程共享進程所擁有的全部資源。線程是處理器調度的基本單  位

  線程安全:當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不  需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼這個類就是線程安全的。

同步和異步關注的是消息通信機制

 同步:是發出一個調用時,在沒有得到結果之前,該調用就不返回,一旦調用返回,就得到返回值。

 異步:而異步則相反,調用在發出之後調用就直接返回,所以沒有返回結果。

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.

 阻塞:阻塞調用是指調用結果返回之前,當前線程會被掛起,調用線程只有在得到結果之後纔會返回。

 非阻塞:調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。

線程分析

順序執行

       應用程序是圍繞執行任務進行管理的,所謂任務就是抽象,離散的工作單元。

應用程序內部的任務調度,最簡單的方式就是單一的線程中順序的執行任務,下圖是在主線程中執行任務。

                                        

       圖中的這個程序是順序執行的,因爲實際執行效率是非常糟糕的,主線程沒有完成網絡請求操作之前後面的執行數據庫耗時操作也必須等待。

異步執行

       爲了提供更好的響應性,處理更多的請求,可以爲每個請求創建一個新的線程。

創建線程有三種方式:

第一種:繼承Thread類創建線程

    首先定義Thread類的子類,並重寫該類的run()方法,該對象的方法體就是現場需要完成的任務,run()方法也稱爲線程執行體,然後創建Thread子類的實例也就是創建線程對象,啓動線程調用Thread的start()方法。

                                                            

第二種:通過實現Runnable接口創建並啓動線程

       首先定義Runnable接口的實現類,一樣要重寫run()方法,這個run()方法和Thread中的run()方法一樣是線程的執行體,然後創建Runnable實現類的實例,並用這個實例作爲Thread的target來創建Thread對象,這個Thread對象纔是真正的線程對象,最後依然是通過調用線程對象的start()方法來啓動線程

                                                    

第三種:使用Callable和Futrue創建線程,這裏我們先不做介紹,在後面介紹Callable和Futrue時再進行講解

                                     

        主線程中爲每個請求都創建了一個新的線程來處理耗時操作,而不是在主線程內部處理它,由此可以得出結論:

執行任務已經脫離了主線程,因此主線程可以迅速的開始處理下一個請求,這樣就提高了響應性

並行處理任務,使得多個請求可以同時得到服務,即使某個線程被阻塞了,其他線程也可以正常工作,程序的吞吐量會得到提高

任務處理的代碼必須是線程安全的,如果再多創建幾個Thread處理相同網絡請求和數據庫操作,那麼如果其中有多個線程公用的數據就會併發的調用它,很可能就會造成數據混亂

 

無限制創建線程的缺點

        在實際的生產環境中,“每個任務每個線程”方式是存在一些缺陷的,尤其在需要創建大量的線程時會更加突出,例如在Android開發中。以下是這種方式的缺點:

  1. 線程生命週期的開銷: 線程的創建與關閉不是“免費”的,創建線程需要時間,因此會帶來處理請求的延遲,並且需要在JVM和操作系統之間進行相應的處理活動。
  2. 資源消耗量:活動的線程會消耗系統資源,尤其是內存,如果可運行的線程數多以可用的處理器數,線程就會閒置,並且大量線程會競爭CPU資源,產生其他開銷。
  3. 穩定性:應該限制可創建線程的數目,如果超出了限制,很可能會收到一個OutOfMemoryError.

Executor框架

Executor介紹

       任務是邏輯上的工作單元,線程是使任務異步執行的機制。

所有任務在單一的線程中順序執行,以及每個任務在自己的線程中執行,每一種策略都有嚴重的侷限性,順序執行會產生糟糕的響應性和吞吐量,“每任務每線程”會給資源管理帶來麻煩。

      在Java類庫中,任務執行的首要抽象不是Thread,而是Executor,查看源碼中Executor的解釋是它爲任務提交和任務執行之間的解耦提供了標準的方法,爲使用Runnable描述任務提供了通用的方式。

      Executor基於生產者-消費者模式,提交任務的執行者是生產者,執行任務的線程是消費者,在程序中實現一個生產者-消費者的設計,使用Executor通常是最簡單的方式

                                       

        Executor接口定義了一個方法excute(Runnable command),在該方法接收一個Runnable實例,用來執行一個任務,任務即一個實現了Runnable接口的類。

ExecutorService介紹

     我們先來看一張類圖結構。

                                          

       可以看到ExecutorService類繼承了Executor,因爲Executor是異步執行任務,所以在任何時間,所有值錢提交的任務的狀態都不能立即可見,既然Executor是爲應用程序執行任務提供服務的,那麼這些任務理應可以被關閉,爲了解決Executor的聲明週期問題,ExecutorService接口擴展了Executor,並且添加了一些用於生命週期管理的方法,同時還有一些用於任務提交的便利方法。

      查看ExecutorService類結構如下圖:

                                   

        從其內部方法中我們看到其實它暗示了ExecutorService的聲明週期有三種狀態,運行(running),關閉(shut down)和終止(terminate)。創建後便進入運行狀態,當調用了shutdown()方法時,便進入關閉狀態, 當所有已經提交了的任務執行完後,便到達終止狀態。如果不調用shutdown()方法,ExecutorService會一直處在運行狀態,不斷接收新的任務,執行新的任務,服務器端一般不需要關閉它,保持一直運行即可。

      shutdown()方法會啓動一個平緩的關閉過程:停止接受新的任務,同時等待已經提交的任務完成---包括尚未開始執行的任務。

      shutdownNow()方法會啓動一個強制的關閉過程:嘗試取消所有運行中的任務和排在隊列中尚未開始的任務。一旦所有任務全部完成後, ExecutorService會轉入終止狀態。

      在ExecutorService中我們看到提交任務的submit()方法可以提交Runable任務對象,也可以提交Callable任務對象,並且submit()方法返回了Future這個類,我們來了解一下這兩個類的具體作用。

      先看一下Callable和Runnable兩個類具體區別,首先我們知道Runnable中的run()方法是沒有返回值的,但是我們看一下Callable可以看到這個任務執行對象的call()方法是有返回值的。

                                                  

       可以看到call()方法有一個泛型的返回值,這個值在call()方法結束時可以return一個返回值,我們在使用Callable時需要實現這個方法。

       接下來我們看看Futrue這個類,Future是我們在使用java實現異步時最常用到的一個類,我們可以向線程池提交一個Callable,並通過future對象獲取執行結果。我們從源碼的角度看一下爲什麼它可以獲取到執行狀態以及執行結果,我們還是先看看這個類的結構。

                                                            

      看到這幾個方法就可以大概判斷出這幾個方法的作用。我們根據源碼中的註釋翻譯一下。

  • cancel():取消一個任務,並返回取消結果,參數表示是否中斷線程。
  • isCanceled():判斷任務是否被取消
  • isDone():判斷當前任務是否執行完畢,包括正常執行完畢,執行異常或者任務取消
  • get():獲取任務執行結果,任務結束之前會阻塞。
  • get():在指定時間內嘗試獲取執行結果,若超時則拋出超時異常

     這裏幾個方法可以看出Future提供了三種功能:

判斷任務是否完成

能夠中斷任務

能夠獲取任務執行結果

    這個Future是一個接口,那麼我們先看看它的實現類。

                                                        

    我們直接看最外面的實現類FutureTask,

   首先看到這個類中成員變量:

                                       

        後面判斷執行狀態時都是根據這個state來進行判斷,並且state是賦值給下面的這些固定常量值。

       接下來我們看一下FutureTask中任務是怎麼執行的,當任務被提交到線程池後,會執行FutureTask的run()方法。具體爲什麼ExecutorService提交任務之後會執行futureTask的run()方法,我們這裏不做講解,只是提供一下源碼查看這個過程涉及到的類以及方法供自己去查看本文使用的是JDK1.8的源碼。查看步驟首先是ExecutorService的實現類AbstractExecutorService中的submit(Callable<T> task)方法,然後進入看到newTaskFor()方法中new FutureTask(),然後執行的是execute(FutureTask)方法,跳到一個具體的線程池類中查看execute()方法,比如ThreadPoolExecutor類中的execute()方法,然後調用if(workerCountOf(c)<corePoolSize)判斷當前線程總數是否小於核心線程數量,如果是true則進入addWorker()方法。

      這個方法中就可以看到使用Thread的start()來執行這個傳遞進來的FutureTask,也就是執行FutureTask的run()方法,run()方法中執行了當前Callable的call()方法,這個Callable可以是直接傳遞進來的也可以是通過Executors.callable(runnable)轉換過來的。在FutureTask的run()方法中我們可以看到在調用call()方法結束後如果業務邏輯異常,則調用setException方法將異常對象賦給outcome,並且更新state值,如果業務正常,則調用set方法將執行結果賦給outcome,並且更新state值,並且是通過UNSAFE.compareAndSwapInt()方法來完成state狀態值的更新,狀態變更的原子性由unsafe對象提供的CAS操作保證,是通過底層庫Native方法來實現的。

         通過以上代碼的流程梳理我們可以知道爲什麼Future可以獲取到執行狀態以及執行結果,下面我們通過一個簡單的例子看一下Callable以及Future如何使用。

                                      

線程池介紹

       ThreadPoolExecutor是一個靈活的,健壯的線程池的實現,允許用戶進行各種各樣的定製。

概念介紹

       我們看一下它的構造函數

                                           

     我們分析一下這幾個參數。

  • corePoolSize:核心池大小就是目標的大小,線程池的實現試圖維護池的大小,即使沒有任務執行,池的代銷也等於核心池的大小,並且直到工作隊列充滿前,池都不會創建更多的線程,當ThreadPoolExecutor被初始化創建後,所有的核心線程並非立即開始,需要等到有任務提交的時刻,除非我們調用prestartAllCoreThreads方法,核心線程在allowCoreThreadTimeout被設置爲true時會超時退出,默認情況下不會退出。
  • maximunPoolSize:最大線程池大小,是同時可以活動的線程數的上限。
  • keepAliveTime:如果線程數多於corePoolSize,則這些多於的線程的空閒時間超過keepAliveTime時將被終止。
  • TimeUnit:線程活動保持時間的單位,TimeUnit.DAYS 天 TimeUnit.HOURS 小時 TimeUnit.MINUTES 分鐘 TimeUnit.SECONDS 秒 TimeUnit.MILLISECONDS 毫秒 TimeUnit.MICROSECONDS 微妙 TimeUnit.NANOSECONDS 納秒
  • workQueue:工作隊列任務隊列這裏需要仔細講解一下,不然後面分析各種線程池時很容易弄混淆。首先一個任務來了需要排隊,這個時候涉及到排隊的策略,任務排隊有三種基本方法:無限隊列,有限隊列,同步移交。
  1. 無限隊列:LinkedBlockingQueue隊列,這是一個鏈表結構所以可以無限延長,如果所有的工作線程都處於忙碌狀態,任務將會在隊列中等候,如果任務持續的快速到達超過了他們被執行的速度,隊列將會無限制的增加,可能會造成資源耗盡的情況。
  2. 有界隊列:ArrayBlockingQueue有界隊列這是一個數組,所以是一個定長的隊列,有界隊列能避免資源耗盡的情況發生,但是當隊列滿了後新的任務怎麼辦。
  3. 同步移交:SynchronousQueue同步移交隊列,這個隊列中不保存任務任務,相當於一個管道的作用,隊列接收到任務的時候,會直接提交給線程處理,而不保留它
  4. 延時隊列:DelayQueue隊列內元素必須實現Delayed接口,這就意味着你傳進去的任務必須先實現Delayed接口,這個隊列接收到任務時,首先先入隊,只有達到了指定的延時時間,纔會執行任務。
  1. RejectedExecutionHandler:飽和策略,當一個有限隊列充滿後,飽和策略就開始起作用了,以及當任務提交到一個已經關閉的Executor時,也會用到飽和策略。

      AbortPolicy:中止策略 會Execute拋出未檢查的RejectExecutorException異常,調用者可以捕獲這個異常,然後按自己的需求處理;

     DiscardPolic:遺棄策略 默認放棄這個任務

     DiscardOldestPolicy:遺棄最舊的策略會選擇丟棄本應該下來就執行的任務,該策略還會嘗試去重新提交新任務。

     CallerRunsPolicy:調用者運行策略 既不會丟棄哪個任務,也不會拋出任何異常,它會把一些任務退回到調用者那裏,以減緩新任務流。

ThreadFactory:線程工廠,線程池總是通過線程工廠來創建線程,我們可以通過自定義線程工廠來實現我們自己的操作。

我們先來看一下創建線程池的時候系統默認使用的線程工廠是什麼樣的

                                 

可以看到其實默認的線程工廠實現很簡單,它做的事就是統一給線程池中的線程設置線程group來分組,以及同意線程前綴名,以及設置線程優先級。

步驟介紹

        我們先看一下線程池時怎麼處理線程任務提交的,下面是步驟圖

 

https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1527672157637&di=dcc0d826aa3c321102ec673b64ea7be0&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D3369160353%2C2107528450%26fm%3D214%26gp%3D0.jpg

    從圖上可以看出來,線程池執行所提交的任務過程主要有這樣幾個階段“

  1. 先判斷線程池中核心線程池所有的線程是否都在執行任務,如果不是,則新建一個線程執行剛提交的任務,否則核心線程池中所有的線程都在執行任務,則進入第二步;
  2. 判斷阻塞隊列是否已滿,如果未滿,則將提交的任務放置在阻塞隊列中,否則進入第三步,這裏需要指出如果是有界隊列纔會出現隊列已滿的情況。
  3. 判斷線程池中所有的線程是否都在執行任務,如果沒有,則創建一個新的線程來執行任務,否則交給飽和策略進行處理

                                  

  Execute()方法執行邏輯有這樣幾種情況:

  1. 如果當前運行的線程少於corePoolSize,則會創建新的線程來執行新的任務;
  2. 如果運行的線程個數等於或者大於corePoolSize,則會將提交的任務存放到阻塞隊列workQueue中;
  3. 如果當前workQueue隊列已滿的話,則會創建新的線程來執行任務
  4. 如果線程個數已經超過了maximumPoolSize,則會使用飽和策略RejectedExecutionHandler來進行處理

線程池應用

       Executors可以創建四種類型的ThreadPoolExecutor線程池,因爲cheduledThreadPoolExecutor比較特殊,WorkStealingPool內部不是使用ThreadPoolExecutor所以這兩個我們就不做分析

FixedThreadPool

       創建固定長度的線程池,每次提交任務創建一個線程,直到達到線程池的最大數量,線程池的大小不再變化。

這個線程池可以創建固定線程數的線程池,特點就是可以重用固定數量線程的線程池,它的構造源碼如下;

  1. FixedThreadPoolExecutor的corePoolSize和maxiumPoolSize都被設置爲創建FixedThreadPool時指定的參數nThreads.
  2. 0L則表示當前線程池中的線程數量操作核心線程的數量時,多餘線程被立即停止
  3. 最後一個參數表示FixedThreadPool使用了無界隊列LinkedBlockingQueue作爲線程池的做工隊列,由於是無界的,當線程池的線程數達到corePoolSize後,新任務將在無界隊列中等待,因此線程池線程數量不會超過corePoolSize,同時maxiumPoolSize也就變成了一個無效的參數,並且運行中的線程池並不會拒絕任務

SingleThreadExecutor

        SingleThreadExecutor是使用單個工作線程的Executor,特點是使用單個工作線程執行任務,它的構造源碼如下。

       SingleThreadExecutor的corePoolSize和maxiumPoolSize都設置爲1,

     執行過程如下;

  1. 如果當前工作中的線程數量少於corePoolSize,就創建一個新的線程來執行任務,這裏corePoolSize爲1,
  2. 當線程池的工作中的線程數量達到了corePoolSize,則將任務加入LinkedBlockingQueue中,這是一個無界隊列
  3. 線程池中的唯一的線程執行完任務後再去隊列中取任務,由於在線程池中只有一個工作線程,所以任務可以按照添加順序執行

CachedThreadPool

       CachedThreadPool是一個“無限容量的線程池,它會根據需要創建新線程,特點是可以根據需要來創建新的線程執行任務,沒有特定的corePool即核心線程爲0,下面是它的構造函數:

        可以看見CachedThreadPool的corePoolSize是0,maxiumPoolSize設置爲Integer.MAX_VALUE,即maximum是無限大的,這裏的keepAliveTime設置爲60秒,意味着空閒的線程最多可以等待任務60秒,否則將被回收。

       CachedThreadPool使用沒有容量的SynchronousQueue作爲線程池的工作隊列,它是一個沒有容量的阻塞隊列,每個插入操作必須等待另一個線程的對應移除操作,這意味着,如果主線程提交任務的速度高於線程池中處理任務的速度時,CachedThreadPool會不斷的創建新線程,極端情況下,CachedThreadPool會因爲創建過多的線程而耗盡CPU資源

      執行過程:

  1. 首先執行SynchronousQueue.offer(Runnable task) 如果在當前的線程池中有空閒的線程正在執行SynchronousQueue.poll(),那麼當前執行SynchronousQueue.offer()的線程與空閒線程執行poll操作配對成功,主線程把任務交給空閒線程執行,execute()方法執行成功,否則執行步驟b
  2. 當前線程池爲空(初始化maximumPool爲空)或者沒有空閒線程,那麼配對失敗,將沒有線程執行SynchronousQueue.poll()操作,這種情況下,線程池會創建一個新的線程執行任務。進入步驟c
  3. 在創建完新的線程後,將會執行poll操作,進行配對,然後執行任務,當任務執行完成之後包活時間60秒以內,如果這個時間內又有新的任務提交進來,如果其他線程還是在工作狀態那麼這個空閒線程將執行新任務,因此長時間不提交任務的CachedThreadPool不會佔用系統資源。這些線程會在等待包活時間之後被回收。

     SynchronousQueue是一個不存儲元素的阻塞隊列,每次進行offer操作時必須等待poll操作,否則不能繼續添加元素。

 

    介紹了上面這些參數的具體意思之後我們就可以利用ThreadPoolExecutor的構造函數來自定義線程池,根據我們的實際使用場景來輸入各種參數自定義自己的線程池。

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