1. 程式人生 > >JAVA多執行緒(三) 執行緒池和鎖的深度化

JAVA多執行緒(三) 執行緒池和鎖的深度化

 github演示程式碼地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/src/main/java/com/kawa/thread

1.執行緒池

 1.1 執行緒池是什麼

Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式都可以使用執行緒池。在開發過程中,合理地使用執行緒池能夠帶來3個好處。
第一:降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

1.2 執行緒池作用

執行緒池是為突然大量爆發的執行緒設計的,通過有限的幾個固定執行緒為大量的操作服務,減少了建立和銷燬執行緒所需的時間,從而提高效率。
如果一個執行緒的時間非常長,就沒必要用執行緒池了(不是不能作長時間操作,而是不宜),況且我們還不能控制執行緒池中執行緒的開始、掛起、和中止。

1.3 執行緒池的分類

JDK1.5之後加入了java.util.concurrent包,java.util.concurrent包的加入給予開發人員開發併發程式以及解決併發問題很大的幫助。這篇文章主要介紹下併發包下的Executor介面,Executor介面雖然作為一個非常舊的介面(JDK1.5 2004年釋出),但是很多程式設計師對於其中的一些原理還是不熟悉,因此寫這篇文章來介紹下Executor介面,同時鞏固下自己的知識。

Executor框架的最頂層實現是ThreadPoolExecutor類,Executors工廠類中提供的newScheduledThreadPool、newFixedThreadPool、newCachedThreadPool方法其實也只是ThreadPoolExecutor的建構函式引數不同而已。通過傳入不同的引數,就可以構造出適用於不同應用場景下的執行緒池,那麼它的底層原理是怎樣實現的呢,這篇就來介紹下ThreadPoolExecutor執行緒池的執行過程。

corePoolSize: 核心池的大小。 當有任務來之後,就會建立一個執行緒去執行任務,當執行緒池中的執行緒數目達到corePoolSize後,就會把到達的任務放到快取隊列當中
maximumPoolSize: 執行緒池最大執行緒數,它表示線上程池中最多能建立多少個執行緒;
keepAliveTime: 表示執行緒沒有任務執行時最多保持多久時間會終止。
unit: 引數keepAliveTime的時間單位,有7種取值

Java通過Executors(jdk1.5併發包)提供四種執行緒池,分別為:
newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。
案例演示:

newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行

 

演示程式碼: https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/src/main/java/com/kawa/thread/threadpool

1.4 執行緒池的原理

提交一個任務到執行緒池中,執行緒池的處理流程如下:
1、判斷執行緒池裡的核心執行緒是否都在執行任務,如果不是(核心執行緒空閒或者還有核心執行緒沒有被建立)則建立一個新的工作執行緒來執行任務。
如果核心執行緒都在執行任務,則進入下個流程。
2、執行緒池判斷工作佇列是否已滿,如果工作佇列沒有滿,則將新提交的任務儲存在這個工作佇列裡。如果工作佇列滿了,則進入下個流程。 3、判斷執行緒池裡的執行緒是否都處於工作狀態,如果沒有,則建立一個新的工作執行緒來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。

 

 

1.5 執行緒池的合理配置

要想合理的配置執行緒池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:
任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
任務的優先順序:高,中和低。
任務的執行時間:長,中和短。
任務的依賴性:是否依賴其他系統資源,如資料庫連線。
任務性質不同的任務可以用不同規模的執行緒池分開處理。CPU密集型任務配置儘可能少的執行緒數量,如配置Ncpu
+1個執行緒的執行緒池。
IO密集型任務則由於需要等待IO操作,執行緒並不是一直在執行任務,則配置儘可能多的執行緒,如2*Ncpu。
混合型的任務,如果可以拆分,則將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,
那麼分解後執行的吞吐率要高於序列執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。
我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數。 優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。它可以讓優先順序高的任務先得到執行,需要注意的是如果一直有優先順序高的任務提交到佇列裡,
那麼優先順序低的任務可能永遠不能執行。 執行時間不同的任務可以交給不同規模的執行緒池來處理,或者也可以使用優先順序佇列,讓執行時間短的任務先執行。 依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼執行緒數應該設定越大,這樣才能更好的利用CPU。 CPU密集型時,任務可以少配置執行緒數,大概和機器的cpu核數相當,這樣可以使得每個執行緒都在執行任務 IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數,
2*cpu核數 作業系統之名稱解釋: 某些程序花費了絕大多數時間在計算上,而其他則在等待I/O上花費了大多是時間, 前者稱為計算密集型(CPU密集型)computer-bound,後者稱為I/O密集型,I/O-bound。

 

2.鎖的深度化

2.1 悲觀鎖,樂觀鎖

悲觀鎖:悲觀鎖悲觀的認為每一次操作都會造成更新丟失問題,在每次查詢時加上排他鎖。
每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會block直到它拿到鎖。
傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。 Select
* from xxx for update; 樂觀鎖:樂觀鎖會樂觀的認為每次查詢都不會造成更新丟失,利用版本欄位控制

2.2 重入鎖

鎖作為併發共享資料,保證一致性的工具,在JAVA平臺有多種實現(如 synchronized 和 ReentrantLock等等 ) 。這些已經寫好提供的鎖為我們開發提供了便利。
重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。
在JAVA環境下 ReentrantLock 和synchronized 都是 可重入鎖

 演示程式碼:https://github.com/showkawa/springBoot_2017/blob/master/spb-demo/src/main/java/com/kawa/thread/lock/ReentrantLockThread.java

2.3 讀寫鎖

相比Java中的鎖(Locks in Java)裡Lock實現,讀寫鎖更復雜一些。假設你的程式中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那麼頻繁。
在沒有寫操作的時候,兩個執行緒同時讀一個資源沒有任何問題,所以應該允許多個執行緒能在同時讀取共享資源。
但是如果有一個執行緒想去寫這些共享資源,就不應該再有其它執行緒對該資源進行讀或寫(也就是說:讀-讀能共存,讀-寫不能共存,寫-寫不能共存)。
這就需要一個讀/寫鎖來解決這個問題。Java5在java.util.concurrent包中已經包含了讀寫鎖。

 演示程式碼:https://github.com/showkawa/springBoot_2017/blob/master/spb-demo/src/main/java/com/kawa/thread/lock/WriteReadLockThread.java

2.4 CAS無鎖機制

 

(1)與鎖相比,使用比較交換(下文簡稱CAS)會使程式看起來更加複雜一些。但由於其非阻塞性,它對死鎖問題天生免疫,並且,執行緒間的相互影響也遠遠比基於鎖的方式要小。
更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統開銷,也沒有執行緒間頻繁排程帶來的開銷,因此,它要比基於鎖的方式擁有更優越的效能。 (
2)無鎖的好處: 第一,在高併發的情況下,它比有鎖的程式擁有更好的效能; 第二,它天生就是死鎖免疫的。 就憑藉這兩個優勢,就值得我們冒險嘗試使用無鎖的併發。 (3)CAS演算法的過程是這樣:它包含三個引數CAS(V,E,N): V表示要更新的變數,E表示預期值,N表示新值。僅當V值等於E值時,才會將V的值設為N,如果V值和E值不同,
則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後,CAS返回當前V的真實值。 (
4)CAS操作是抱著樂觀的態度進行的,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。
失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,
並進行恰當的處理。

 

2.5 自旋鎖

自旋鎖是採用讓當前執行緒不停地的在迴圈體內執行實現的,當迴圈的條件被其他執行緒改變時 才能進入臨界區。

 

public class Test implements Runnable {
    static int sum;
    private SpinLock lock;

    public Test(SpinLock lock) {
        this.lock = lock;
    }

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        SpinLock lock = new SpinLock();
        for (int i = 0; i < 100; i++) {
            Test test = new Test(lock);
            Thread t = new Thread(test);
            t.start();
        }

        Thread.currentThread().sleep(1000);
        System.out.println(sum);
    }

    @Override
    public void run() {
        this.lock.lock();

           this.lock.lock();

           sum++;

           this.lock.unlock();

           this.lock.unlock();

     }

}

當一個執行緒 呼叫這個不可重入的自旋鎖去加鎖的時候沒問題,當再次呼叫lock()的時候,因為自旋鎖的持有引用已經不為空了,該執行緒物件會誤認為是別人的執行緒持有了自旋鎖

使用了CAS原子操作,lock函式將owner設定為當前執行緒,並且預測原來的值為空。unlock函式將owner設定為null,並且預測值為當前執行緒。

當有第二個執行緒呼叫lock操作時由於owner值不為空,導致迴圈一直被執行,直至第一個執行緒呼叫unlock函式將owner設定為null,第二個執行緒才能進入臨界區。

由於自旋鎖只是將當前執行緒不停地執行迴圈體,不進行執行緒狀態的改變,所以響應速度更快。但當執行緒數不停增加時,效能下降明顯,因為每個執行緒都需要執行,佔用CPU時間。如果執行緒競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。