線程池
什么事線程池
Java中的線程池是運用場景最多的并發(fā)框架娶桦,幾乎所有需要異步或并發(fā)執(zhí)行任務(wù)的程序都可以使用線程池泞边。在開發(fā)過程中馍资,合理地使用線程池能夠帶來3個好處穴店。
- 降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗迈嘹。
- 提高響應(yīng)速度削彬。當任務(wù)到達時,任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行秀仲。
- 提高線程的可管理性融痛。線程是稀缺資源,如果無限制地創(chuàng)建神僵,不僅會消耗系統(tǒng)資源雁刷,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一分配保礼、調(diào)優(yōu)和監(jiān)控沛励。但是,要做到合理利用線程池炮障,必須對其實現(xiàn)原理了如指掌目派。
線程池的作用
線程池是為突然大量爆發(fā)的線程設(shè)計的,通過有限的幾個固定線程為大量的操作服務(wù)胁赢,減少了創(chuàng)建和銷毀線程所需的時間企蹭,從而提高效率。
如果一個線程的時間非常長,就沒必要用線程池了(不是不能作長時間操作谅摄,而是不宜徒河。),況且我們還不能控制線程池中線程的開始螟凭、掛起虚青、和中止它呀。
線程池的分類
ThreadPoolExecutor
Executor框架的最頂層實現(xiàn)是ThreadPoolExecutor類螺男,Executors工廠類中提供的newScheduledThreadPool、newFixedThreadPool纵穿、newCachedThreadPool方法其實也只是ThreadPoolExecutor的構(gòu)造函數(shù)參數(shù)不同而已下隧。通過傳入不同的參數(shù),就可以構(gòu)造出適用于不同應(yīng)用場景下的線程池谓媒。
- corePoolSize: 核心池的大小淆院。 當有任務(wù)來之后,就會創(chuàng)建一個線程去執(zhí)行任務(wù)句惯,當線程池中的線程數(shù)目達到corePoolSize后土辩,就會把到達的任務(wù)放到緩存隊列當中。
- maximumPoolSize: 線程池最大線程數(shù)抢野,它表示在線程池中最多能創(chuàng)建多少個線程拷淘。
- keepAliveTime: 表示線程沒有任務(wù)執(zhí)行時最多保持多久時間會終止。
- unit: 參數(shù)keepAliveTime的時間單位指孤,有7種取值启涯,在TimeUnit類中有7種靜態(tài)屬性。
創(chuàng)建線程池的四中方式
- newCachedThreadPool創(chuàng)建一個可緩存線程池恃轩,如果線程池長度超過處理需要结洼,可靈活回收空閑線程,若無可回收叉跛,則新建線程松忍。
- newFixedThreadPool 創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù)筷厘,超出的線程會在隊列中等待鸣峭。
- newScheduledThreadPool 創(chuàng)建一個定長線程池,支持定時及周期性任務(wù)執(zhí)行敞掘。
- newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池叽掘,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行玖雁。
package top.nightliar.study.day05;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 線程池的四種創(chuàng)建方法
* Created by Nightliar
* 2018-09-20 10:46
*/
public class ThreadPoolDemo01 {
public static void main(String[] args) {
// 1. 可緩存更扁,無限大小的線程池
TestCachedThreadPool();
// 2. 定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待
TestFixedThreadPool();
// 3. 創(chuàng)建一個定長線程池浓镜,支持定時及周期性任務(wù)執(zhí)行
TestScheduledThreadPool();
// 4. 單線程化的線程池溃列,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行
TestSingleThreadExecutor();
}
private static void TestCachedThreadPool(){
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int temp = i; // 這里如果不更改temp的值膛薛,那么jvm中默認temp為final類型
cachedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + "听隐,i:" + temp));
}
cachedThreadPool.shutdown();
}
private static void TestFixedThreadPool(){
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
int temp = i; // 這里如果不更改temp的值,那么jvm中默認temp為final類型
fixedThreadPool.execute(() -> System.out.println(Thread.currentThread().getName() + "哄啄,i:" + temp));
}
fixedThreadPool.shutdown();
}
private static void TestScheduledThreadPool(){
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
final int temp = i;
// 表示3秒后一起執(zhí)行
scheduledThreadPool.schedule(() -> System.out.println(Thread.currentThread().getName() + "雅任,i:" + temp), 3, TimeUnit.SECONDS);
}
scheduledThreadPool.shutdown();
}
private static void TestSingleThreadExecutor(){
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int temp = i;
// 單線程執(zhí)行
singleThreadExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + ",i:" + temp);
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
});
}
singleThreadExecutor.shutdown();
}
}
線程池的原理
提交一個任務(wù)到線程池中咨跌,線程池的處理流程如下:
- 判斷線程池里的核心線程是否都在執(zhí)行任務(wù)沪么,如果不是(核心線程空閑或者還有核心線程沒有被創(chuàng)建)則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)。如果核心線程都在執(zhí)行任務(wù)锌半,則進入下個流程禽车。
- 線程池判斷工作隊列是否已滿,如果工作隊列沒有滿刊殉,則將新提交的任務(wù)存儲在這個工作隊列里殉摔。如果工作隊列滿了,則進入下個流程记焊。
- 判斷線程池里的線程是否都處于工作狀態(tài)逸月,如果沒有,則創(chuàng)建一個新的工作線程來執(zhí)行任務(wù)亚亲。如果已經(jīng)滿了彻采,則交給飽和策略來處理這個任務(wù)。
合理配置線程池
要想合理的配置線程池捌归,就必須首先分析任務(wù)特性肛响,可以從以下幾個角度來進行分析:
- 任務(wù)的性質(zhì):CPU密集型任務(wù),IO密集型任務(wù)和混合型任務(wù)惜索。
- 任務(wù)的優(yōu)先級:高特笋,中和低。
- 任務(wù)的執(zhí)行時間:長巾兆,中和短猎物。
- 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接角塑。
- CPU密集型時蔫磨,任務(wù)可以少配置線程數(shù),大概和機器的cpu核數(shù)相當圃伶,這樣可以使得每個線程都在執(zhí)行任務(wù)
- IO密集型時堤如,大部分線程都阻塞蒲列,故需要多配置線程數(shù),2*cpu核數(shù)
某些進程花費了絕大多數(shù)時間在計算上搀罢,而其他則在等待I/O上花費了大多是時間蝗岖,前者稱為計算密集型(CPU密集型)computer-bound,后者稱為I/O密集型榔至,I/O-bound抵赢。
Java鎖的深度化
悲觀鎖
每次去拿數(shù)據(jù)的時候都認為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖唧取,這樣別人想拿這個數(shù)據(jù)就會block直到它拿到鎖铅鲤。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機制,比如行鎖兵怯,表鎖等彩匕,讀鎖腔剂,寫鎖等媒区,都是在做操作之前先上鎖。
Select * from xxx for update;
悲觀鎖:悲觀鎖悲觀的認為每一次操作都會造成更新丟失問題掸犬,在每次查詢時加上排他鎖袜漩。
樂觀鎖
樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制湾碎。悲觀鎖大多數(shù)情況下依靠數(shù)據(jù)庫的鎖機制實現(xiàn)宙攻,以保證操作最大程度的獨占性。但隨之而來的就是數(shù)據(jù)庫性能的大量開銷介褥,特別是對長事務(wù)而言座掘,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題柔滔。樂觀鎖溢陪,大多是基于數(shù)據(jù)版本( Version )記錄機制實現(xiàn)。何謂數(shù)據(jù)版本睛廊?即為數(shù)據(jù)增加一個版本標識形真,在基于數(shù)據(jù)庫表的版本解決方案中,一般是通過為數(shù)據(jù)庫表增加一個 “version” 字段來實現(xiàn)超全。讀取出數(shù)據(jù)時咆霜,將此版本號一同讀出,之后更新時嘶朱,對此版本號加一蛾坯。此時,將提交數(shù)據(jù)的版本數(shù)據(jù)與數(shù)據(jù)庫表對應(yīng)記錄的當前版本信息進行比對疏遏,如果提交的數(shù)據(jù)版本號大于數(shù)據(jù)庫表當前版本號脉课,則予以更新挂疆,否則認為是過期數(shù)據(jù)。
重入鎖
鎖作為并發(fā)共享數(shù)據(jù)下翎,保證一致性的工具缤言,在JAVA平臺有多種實現(xiàn)(如 synchronized 和 ReentrantLock等等 ) 。這些已經(jīng)寫好提供的鎖為我們開發(fā)提供了便利视事。
重入鎖胆萧,也叫做遞歸鎖,指的是同一線程 外層函數(shù)獲得鎖之后 俐东,內(nèi)層遞歸函數(shù)仍然有獲取該鎖的代碼跌穗,但不受影響。
package top.nightliar.study.day05;
import java.util.concurrent.locks.ReentrantLock;
/**
* 重入鎖
* Created by Nightliar
* 2018-09-20 16:14
*/
public class LockDemo01 {
public static void main(String[] args) {
Syn syn = new Syn();
new Thread(syn).start();
new Thread(syn).start();
new Thread(syn).start();
Lok lok = new Lok();
new Thread(lok).start();
new Thread(lok).start();
new Thread(lok).start();
}
}
class Syn implements Runnable {
@Override
public void run() {
get();
}
private synchronized void get() {
System.out.println(Thread.currentThread().getName() + ", get");
set();
}
private synchronized void set() {
System.out.println(Thread.currentThread().getName() + ", set");
}
}
class Lok implements Runnable {
ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
System.out.println(Thread.currentThread().getName() + ", get");
set();
lock.unlock();
}
private void set() {
lock.lock();
System.out.println(Thread.currentThread().getName() + ", set");
lock.unlock();
}
}
讀寫鎖
相比Java中的鎖(Locks in Java)里Lock實現(xiàn)虏辫,讀寫鎖更復(fù)雜一些蚌吸。假設(shè)你的程序中涉及到對一些共享資源的讀和寫操作,且寫操作沒有讀操作那么頻繁砌庄。在沒有寫操作的時候羹唠,兩個線程同時讀一個資源沒有任何問題,所以應(yīng)該允許多個線程能在同時讀取共享資源娄昆。但是如果有一個線程想去寫這些共享資源佩微,就不應(yīng)該再有其它線程對該資源進行讀或?qū)?/strong>(注:也就是說:讀-讀能共存,讀-寫不能共存萌焰,寫-寫不能共存)哺眯。這就需要一個讀/寫鎖來解決這個問題。Java5在java.util.concurrent包中已經(jīng)包含了讀寫鎖扒俯。
package top.nightliar.study.day05;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 讀寫鎖
* Created by Nightliar
* 2018-09-20 16:26
*/
public class LockDemo02 {
public static void main(String[] args) {
new Thread(() ->{
for (int i = 0; i < 10; i++) {
Cache.set(i+"", i+"");
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
Cache.get(i+"");
}
}).start();
}
}
class Cache {
static volatile Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
/**
* 讀
*/
public static Object get(String key){
r.lock();
Object obj = null;
try {
System.out.println("正在做讀的操作,key:" + key + " 開始");
Thread.sleep(100);
obj = map.get(key);
System.out.println("正在做讀的操作,key:" + key + " 結(jié)束奶卓,value="+ obj);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
return obj;
}
/**
* 寫
*/
public static Object set(String key, Object value){
w.lock();
Object obj = value;
try {
System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "開始.");
Thread.sleep(100);
obj = map.put(key, value); // 返回舊有的value
System.out.println("正在做寫的操作,key:" + key + ",value:" + value + "結(jié)束.");
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
return obj;
}
}
CAS無鎖機制
- 與鎖相比,使用比較交換(下文簡稱CAS)會使程序看起來更加復(fù)雜一些撼玄。但由于其非阻塞性夺姑,它對死鎖問題天生免疫,并且互纯,線程間的相互影響也遠遠比基于鎖的方式要小瑟幕。更為重要的是,使用無鎖的方式完全沒有鎖競爭帶來的系統(tǒng)開銷留潦,也沒有線程間頻繁調(diào)度帶來的開銷只盹,因此,它要比基于鎖的方式擁有更優(yōu)越的性能兔院。
- 無鎖的好處:第一殖卑,在高并發(fā)的情況下,它比有鎖的程序擁有更好的性能坊萝;第二孵稽,它天生就是死鎖免疫的许起。
- CAS算法的過程:它包含三個參數(shù)CAS(V,E,N): V表示要更新的變量,E表示預(yù)期值菩鲜,N表示新值园细。僅當V值等于E值時,才會將V的值設(shè)為N接校,如果V值和E值不同猛频,則說明已經(jīng)有其他線程做了更新,則當前線程什么都不做蛛勉。最后鹿寻,CAS返回當前V的真實值。
- CAS操作是抱著樂觀的態(tài)度進行的诽凌,它總是認為自己可以成功完成操作毡熏。當多個線程同時使用CAS操作一個變量時,只有一個會勝出侣诵,并成功更新痢法,其余均會失敗。失敗的線程不會被掛起窝趣,僅是被告知失敗疯暑,并且允許再次嘗試,當然也允許失敗的線程放棄操作哑舒。基于這樣的原理幻馁,CAS操作即使沒有鎖洗鸵,也可以發(fā)現(xiàn)其他線程對當前線程的干擾,并進行恰當?shù)奶幚怼?/li>
- 簡單地說仗嗦,CAS需要你額外給出一個期望值膘滨,也就是你認為這個變量現(xiàn)在應(yīng)該是什么樣子的。如果變量不是你想象的那樣稀拐,那說明它已經(jīng)被別人修改過了火邓。你就重新讀取,再次嘗試修改就好了德撬。
- 在硬件層面铲咨,大部分的現(xiàn)代處理器都已經(jīng)支持原子化的CAS指令。在JDK 5.0以后蜓洪,虛擬機便可以使用這個指令來實現(xiàn)并發(fā)操作和并發(fā)數(shù)據(jù)結(jié)構(gòu)纤勒,并且,這種操作在虛擬機中可以說是無處不在隆檀。
自旋鎖
自旋鎖是采用讓當前線程不停地的在循環(huán)體內(nèi)執(zhí)行實現(xiàn)的摇天,當循環(huán)的條件被其他線程改變時 才能進入臨界區(qū)粹湃。
自旋鎖只是將當前線程不停地執(zhí)行循環(huán)體,不進行線程狀態(tài)的改變泉坐,所以響應(yīng)速度更快为鳄。但當線程數(shù)不停增加時,性能下降明顯腕让,因為每個線程都需要執(zhí)行济赎,占用CPU時間。如果線程競爭不激烈记某,并且保持鎖的時間段司训。適合使用自旋鎖。