天天看點

JAVA多線程(三) 線程池和鎖的深度化

 github示範代碼位址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-service/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/spb-brian-query-service/src/main/java/com/kawa/thread/threadpool

1.4 線程池的原理

送出一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池裡的核心線程是否都在執行任務,如果不是(核心線程空閑或者還有核心線程沒有被建立)則建立一個新的工作線程來執行任務。
如果核心線程都在執行任務,則進入下個流程。
2、線程池判斷工作隊列是否已滿,如果工作隊列沒有滿,則将新送出的任務存儲在這個工作隊列裡。如果工作隊列滿了,則進入下個流程。
3、判斷線程池裡的線程是否都處于工作狀态,如果沒有,則建立一個新的工作線程來執行任務。如果已經滿了,則交給飽和政策來處理這個任務。      
JAVA多線程(三) 線程池和鎖的深度化

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/spb-brian-query-service/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/spb-brian-query-service/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時間。如果線程競争不激烈,并且保持鎖的時間段。适合使用自旋鎖。

轉載于:https://www.cnblogs.com/hlkawa/p/9872136.html