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時間。如果線程競爭不激烈,並且保持鎖的時間段。適合使用自旋鎖。
JAVA多線程(三) 線程池和鎖的深度化