戳我的筆記鏈接地址
本文是對(duì)《Java并發(fā)編程》專(zhuān)欄的讀后小結(jié)尝偎,跟大家分享。
目錄
1镜沽、bug的源頭-三個(gè)屬性
2、Java內(nèi)存模型
3、死鎖的解決方案
死鎖發(fā)生的條件
死鎖的預(yù)防
4盗胀、等待-通知機(jī)制
wait的使用范式
wait和sleep的區(qū)別
5、線程的生命周期
通用的線程生命周期(五態(tài)模型)
Java中線程的生命周期
狀態(tài)轉(zhuǎn)換
6锄贼、創(chuàng)建合理的線程數(shù)量
CPU密集型應(yīng)用
I/O密集型應(yīng)用
7票灰、Lock與synchronized的不同
用兩個(gè)條件變量實(shí)現(xiàn)阻塞隊(duì)列
異步轉(zhuǎn)同步
8、用Semaphore實(shí)現(xiàn)一個(gè)限流器
9宅荤、讀寫(xiě)鎖 ReadWriteLock
讀寫(xiě)鎖升級(jí)問(wèn)題
9屑迂、StampedLock 比讀寫(xiě)鎖更快的鎖
10、CountDownLatch 和 CyclicBarrier 讓多線程步調(diào)一致
11冯键、Java并發(fā)容器
注意事項(xiàng)
12惹盼、原子類(lèi)
ABA問(wèn)題
原子類(lèi)組成概覽
13、Java中的線程池
// todo 線程的運(yùn)行過(guò)程
ThreadPoolExecutor 線程池參數(shù)
拒絕策略
使用線程池的注意事項(xiàng)
14惫确、Future 獲取異步執(zhí)行結(jié)果
如何獲取異步任務(wù)執(zhí)行結(jié)果
FutureTask工具類(lèi)
14手报、CompletableFuture 異步編程
15、CompletionService 批量執(zhí)行異步任務(wù)
16改化、Fork/Join 并行計(jì)算框架
模擬MapReduce統(tǒng)計(jì)單詞數(shù)量
1掩蛤、bug的源頭-三個(gè)屬性
可見(jiàn)性、有序性陈肛、原子性揍鸟。
我們的 CPU、內(nèi)存句旱、I/O 設(shè)備都在不斷迭代阳藻,不斷朝著更快的方向努力晰奖。但是,在這個(gè)快速發(fā)展的過(guò)程中腥泥,有一個(gè)核心矛盾一直存在畅涂,就是這三者的速度差異。
cpu寄存器緩存導(dǎo)致的可見(jiàn)性問(wèn)題道川。
線程切換帶來(lái)原子性問(wèn)題午衰。
編譯優(yōu)化帶來(lái)有序性問(wèn)題。
其中冒萄,在 Java 領(lǐng)域一個(gè)經(jīng)典的案例就是利用雙重檢查創(chuàng)建單例對(duì)象:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton(); //在這一步如果編譯優(yōu)化
}
}
return instance;
}
}
第八行臊岸,如果發(fā)生編譯優(yōu)化:我們以為的 new 操作應(yīng)該是:
分配一塊內(nèi)存 M;
在內(nèi)存 M 上初始化 Singleton 對(duì)象尊流;
然后 M 的地址賦值給 instance 變量帅戒。
但是實(shí)際上優(yōu)化后的執(zhí)行路徑卻是這樣的:
分配一塊內(nèi)存 M;
將 M 的地址賦值給 instance 變量崖技;
最后在內(nèi)存 M 上初始化 Singleton 對(duì)象逻住。
如果在第2步發(fā)生線程a到線程b的切換,線程b直接返回instance迎献,這個(gè)時(shí)候調(diào)用沒(méi)有初始化過(guò)的instance對(duì)象瞎访,會(huì)產(chǎn)生空指針異常。
解決辦法:對(duì)instance對(duì)象加volatile語(yǔ)義申明吁恍。
2扒秸、Java內(nèi)存模型
Java 內(nèi)存模型規(guī)范了 JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法。具體來(lái)說(shuō)冀瓦,這些方法包括 volatile伴奥、synchronized 和 final 三個(gè)關(guān)鍵字,以及七項(xiàng) Happens-Before 規(guī)則翼闽。
3拾徙、死鎖的解決方案
使用細(xì)粒度鎖可以提高并行度,是性能優(yōu)化的一個(gè)重要手段感局。但是有時(shí)細(xì)粒度的鎖容易導(dǎo)致死鎖尼啡。死鎖的一個(gè)比較專(zhuān)業(yè)的定義是:一組互相競(jìng)爭(zhēng)資源的線程因互相等待,導(dǎo)致“永久”阻塞的現(xiàn)象蓝厌。
死鎖發(fā)生的條件
互斥玄叠,共享資源 X 和 Y 只能被一個(gè)線程占用古徒;
占有且等待拓提,線程 T1 已經(jīng)取得共享資源 X,在等待共享資源 Y 的時(shí)候隧膘,不釋放共享資源 X代态;
不可搶占寺惫,其他線程不能強(qiáng)行搶占線程 T1 占有的資源;
循環(huán)等待蹦疑,線程 T1 等待線程 T2 占有的資源西雀,線程 T2 等待線程 T1 占有的資源,就是循環(huán)等待歉摧。
死鎖的預(yù)防
反過(guò)來(lái)分析艇肴,也就是說(shuō)只要我們破壞其中一個(gè),就可以成功避免死鎖的發(fā)生叁温≡俚浚互斥就是鎖的目的,所以無(wú)法預(yù)防膝但。
破壞占有且等待冲九,可以一次性申請(qǐng)所有資源。要么全部獲取成功跟束,要么全部獲取失敗莺奸。
破壞不可搶占,核心是要能夠主動(dòng)釋放它占有的資源冀宴,這一點(diǎn) synchronized 是做不到的灭贷。java.util.concurrent 這個(gè)包下面提供的 Lock 是可以輕松解決這個(gè)問(wèn)題的。提供tryLock(long, TimeUnit) 方法略贮,在一段時(shí)間后放棄獲取鎖氧腰。
破壞循環(huán)等待條件,破壞這個(gè)條件刨肃,需要對(duì)資源進(jìn)行排序古拴,然后按序申請(qǐng)資源。
4真友、等待-通知機(jī)制
用 synchronized 實(shí)現(xiàn)等待 - 通知機(jī)制在 Java 語(yǔ)言里黄痪,等待 - 通知機(jī)制可以有多種實(shí)現(xiàn)方式,比如 Java 語(yǔ)言?xún)?nèi)置的 synchronized 配合 wait()盔然、notify()桅打、notifyAll() 這三個(gè)方法就能輕松實(shí)現(xiàn)。
如何用 synchronized 實(shí)現(xiàn)互斥鎖愈案,你應(yīng)該已經(jīng)很熟悉了挺尾。在下面這個(gè)圖里,左邊有一個(gè)等待隊(duì)列站绪,同一時(shí)刻遭铺,只允許一個(gè)線程進(jìn)入 synchronized 保護(hù)的臨界區(qū)(這個(gè)臨界區(qū)可以看作大夫的診室),當(dāng)有一個(gè)線程進(jìn)入臨界區(qū)后,其他線程就只能進(jìn)入圖中左邊的等待隊(duì)列里等待(相當(dāng)于患者分診等待)魂挂。這個(gè)等待隊(duì)列和互斥鎖是一對(duì)一的關(guān)系甫题,每個(gè)互斥鎖都有自己獨(dú)立的等待隊(duì)列。
wait()工作原理圖
notify()工作原理圖
上面我們一直強(qiáng)調(diào) wait()涂召、notify()坠非、notifyAll() 方法操作的等待隊(duì)列是互斥鎖的等待隊(duì)列,所以如果 synchronized 鎖定的是 this果正,那么對(duì)應(yīng)的一定是 this.wait()炎码、this.notify()、this.notifyAll()秋泳;如果 synchronized 鎖定的是 target辅肾,那么對(duì)應(yīng)的一定是 target.wait()、target.notify()轮锥、target.notifyAll() 矫钓。
而且 wait()、notify()舍杜、notifyAll() 這三個(gè)方法能夠被調(diào)用的前提是已經(jīng)獲取了相應(yīng)的互斥鎖新娜,所以我們會(huì)發(fā)現(xiàn) wait()、notify()既绩、notifyAll() 都是在 synchronized{}內(nèi)部被調(diào)用的概龄。如果在 synchronized{}外部調(diào)用,或者鎖定的 this饲握,而用 target.wait() 調(diào)用的話私杜,JVM 會(huì)拋出一個(gè)運(yùn)行時(shí)異常:java.lang.IllegalMonitorStateException。
// wait的使用范式
while(條件不滿足) {
wait();
}
除非經(jīng)過(guò)深思熟慮救欧,否則盡量使用 notifyAll()衰粹,只用notify()可能會(huì)導(dǎo)致有線程永遠(yuǎn)得不到執(zhí)行。
wait和sleep的區(qū)別
wait與sleep區(qū)別在于: 1. wait會(huì)釋放所有鎖而sleep不會(huì)釋放鎖資源. 2. wait只能在同步方法和同步塊中使用笆怠,而sleep任何地方都可以. 3. wait無(wú)需捕捉異常铝耻,而sleep需要. 兩者相同點(diǎn):都會(huì)讓渡CPU執(zhí)行時(shí)間蹬刷,等待再次調(diào)度瓢捉! wait()方法與sleep()方法的不同之處在于,wait()方法會(huì)釋放對(duì)象的“鎖標(biāo)志”办成。當(dāng)調(diào)用某一對(duì)象的wait()方法后泡态,會(huì)使當(dāng)前線程暫停執(zhí)行固阁,并將當(dāng)前線程放入對(duì)象等待池中决记,直到調(diào)用了notify()方法后,將從對(duì)象等待池中移出任意一個(gè)線程并放入鎖標(biāo)志等待池中擎勘,只有鎖標(biāo)志等待池中的線程可以獲取鎖標(biāo)志,它們隨時(shí)準(zhǔn)備爭(zhēng)奪鎖的擁有權(quán)刀崖。當(dāng)調(diào)用了某個(gè)對(duì)象的notifyAll()方法惊科,會(huì)將對(duì)象等待池中的所有線程都移動(dòng)到該對(duì)象的鎖標(biāo)志等待池拍摇。 sleep()方法需要指定等待的時(shí)間亮钦,它可以讓當(dāng)前正在執(zhí)行的線程在指定的時(shí)間內(nèi)暫停執(zhí)行,進(jìn)入阻塞狀態(tài)充活,該方法既可以讓其他同優(yōu)先級(jí)或者高優(yōu)先級(jí)的線程得到執(zhí)行的機(jī)會(huì)蜂莉,也可以讓低優(yōu)先級(jí)的線程得到執(zhí)行機(jī)會(huì)。但是sleep()方法不會(huì)釋放“鎖標(biāo)志”混卵,也就是說(shuō)如果有synchronized同步塊映穗,其他線程仍然不能訪問(wèn)共享數(shù)據(jù)。
5幕随、線程的生命周期
通用的線程生命周期(五態(tài)模型)
初始狀態(tài)蚁滋,指的是線程已經(jīng)被創(chuàng)建,但是還不允許分配 CPU 執(zhí)行赘淮。這個(gè)狀態(tài)屬于編程語(yǔ)言特有的辕录,在操作系統(tǒng)層面,真正的線程還沒(méi)有創(chuàng)建梢卸。
可運(yùn)行狀態(tài)走诞,指的是線程可以分配 CPU 執(zhí)行。在這種狀態(tài)下蛤高,真正的操作系統(tǒng)線程已經(jīng)被成功創(chuàng)建了蚣旱,所以可以分配 CPU 執(zhí)行。
當(dāng)有空閑的 CPU 時(shí)戴陡,操作系統(tǒng)會(huì)將其分配給一個(gè)處于可運(yùn)行狀態(tài)的線程塞绿,被分配到 CPU 的線程的狀態(tài)就轉(zhuǎn)換成了運(yùn)行狀態(tài)。
運(yùn)行狀態(tài)的線程如果調(diào)用一個(gè)阻塞的 API(例如以阻塞方式讀文件)或者等待某個(gè)事件(例如條件變量)恤批,那么線程的狀態(tài)就會(huì)轉(zhuǎn)換到休眠狀態(tài)位隶,同時(shí)釋放 CPU 使用權(quán),休眠狀態(tài)的線程永遠(yuǎn)沒(méi)有機(jī)會(huì)獲得 CPU 使用權(quán)开皿。當(dāng)?shù)却氖录霈F(xiàn)了涧黄,線程就會(huì)從休眠狀態(tài)轉(zhuǎn)換到可運(yùn)行狀態(tài)。
線程執(zhí)行完或者出現(xiàn)異常就會(huì)進(jìn)入終止?fàn)顟B(tài)赋荆,終止?fàn)顟B(tài)的線程不會(huì)切換到其他任何狀態(tài)笋妥,進(jìn)入終止?fàn)顟B(tài)也就意味著線程的生命周期結(jié)束了。
Java中線程的生命周期
這五種狀態(tài)在不同編程語(yǔ)言里會(huì)有簡(jiǎn)化合并窄潭。
Java 語(yǔ)言里則把可運(yùn)行狀態(tài)和運(yùn)行狀態(tài)合并了(變成了運(yùn)行狀態(tài))春宣,這兩個(gè)狀態(tài)在操作系統(tǒng)調(diào)度層面有用,而 JVM 層面不關(guān)心這兩個(gè)狀態(tài),因?yàn)?JVM 把線程調(diào)度交給操作系統(tǒng)處理了月帝。除了簡(jiǎn)化合并躏惋,這五種狀態(tài)也有可能被細(xì)化,比如嚷辅,Java 語(yǔ)言里就細(xì)化了休眠狀態(tài)(Blocked簿姨、Waiting、Timed_Waiting).
Java 語(yǔ)言中線程共有六種狀態(tài)簸搞,分別是:
NEW(初始化狀態(tài))
RUNNABLE(可運(yùn)行 / 運(yùn)行狀態(tài))
BLOCKED(阻塞狀態(tài))
WAITING(無(wú)時(shí)限等待)
TIMED_WAITING(有時(shí)限等待)
TERMINATED(終止?fàn)顟B(tài))
狀態(tài)轉(zhuǎn)換
RUNNABLE 與 BLOCKED 的狀態(tài)轉(zhuǎn)換
只有一種場(chǎng)景會(huì)觸發(fā)這種轉(zhuǎn)換扁位,就是線程等待 synchronized 的隱式鎖。RUNNABLE 與 WAITING 的狀態(tài)轉(zhuǎn)換
第一種場(chǎng)景趁俊,獲得 synchronized 隱式鎖的線程域仇,調(diào)用無(wú)參數(shù)的 Object.wait() 方法。
第二種場(chǎng)景寺擂,調(diào)用無(wú)參數(shù)的 Thread.join() 方法暇务。其中的 join() 是一種線程同步方法,例如有一個(gè)線程對(duì)象 thread A怔软,當(dāng)調(diào)用 A.join() 的時(shí)候垦细,執(zhí)行這條語(yǔ)句的線程會(huì)等待 thread A 執(zhí)行完,而等待中的這個(gè)線程爽雄,其狀態(tài)會(huì)從 RUNNABLE 轉(zhuǎn)換到 WAITING蝠检。當(dāng)線程 thread A 執(zhí)行完,原來(lái)等待它的線程又會(huì)從 WAITING 狀態(tài)轉(zhuǎn)換到 RUNNABLE挚瘟。
第三種場(chǎng)景叹谁,調(diào)用 LockSupport.park() 方法。其中的 LockSupport 對(duì)象乘盖,也許你有點(diǎn)陌生焰檩,其實(shí) Java 并發(fā)包中的鎖,都是基于它實(shí)現(xiàn)的订框。調(diào)用 LockSupport.park() 方法析苫,當(dāng)前線程會(huì)阻塞,線程的狀態(tài)會(huì)從 RUNNABLE 轉(zhuǎn)換到 WAITING穿扳。調(diào)用 LockSupport.unpark(Thread thread) 可喚醒目標(biāo)線程衩侥,目標(biāo)線程的狀態(tài)又會(huì)從 WAITING 狀態(tài)轉(zhuǎn)換到 RUNNABLE。RUNNABLE 與 TIMED_WAITING 的狀態(tài)轉(zhuǎn)換
有五種場(chǎng)景會(huì)觸發(fā)這種轉(zhuǎn)換:
調(diào)用帶超時(shí)參數(shù)的 Thread.sleep(long millis) 方法矛物;
獲得 synchronized 隱式鎖的線程茫死,調(diào)用帶超時(shí)參數(shù)的 Object.wait(long timeout) 方法;
調(diào)用帶超時(shí)參數(shù)的 Thread.join(long millis) 方法履羞;
調(diào)用帶超時(shí)參數(shù)的 LockSupport.parkNanos(Object blocker, long deadline) 方法峦萎;
調(diào)用帶超時(shí)參數(shù)的 LockSupport.parkUntil(long deadline) 方法屡久。
這里你會(huì)發(fā)現(xiàn) TIMED_WAITING 和 WAITING 狀態(tài)的區(qū)別,僅僅是觸發(fā)條件多了超時(shí)參數(shù)爱榔。從 NEW 到 RUNNABLE 狀態(tài)
從 NEW 狀態(tài)轉(zhuǎn)換到 RUNNABLE 狀態(tài)很簡(jiǎn)單被环,只要調(diào)用線程對(duì)象的 start() 方法就可以了從 RUNNABLE 到 TERMINATED 狀態(tài)
線程執(zhí)行完 run() 方法后,會(huì)自動(dòng)轉(zhuǎn)換到 TERMINATED 狀態(tài)详幽,當(dāng)然如果執(zhí)行 run() 方法的時(shí)候異常拋出筛欢,也會(huì)導(dǎo)致線程終止。有時(shí)候我們需要強(qiáng)制中斷 run() 方法的執(zhí)行妒潭,例如 run() 方法訪問(wèn)一個(gè)很慢的網(wǎng)絡(luò)悴能,我們等不下去了揣钦,想終止怎么辦呢雳灾?Java 的 Thread 類(lèi)里面倒是有個(gè) stop() 方法,不過(guò)已經(jīng)標(biāo)記為 @Deprecated冯凹,所以不建議使用了谎亩。正確的姿勢(shì)其實(shí)是調(diào)用 interrupt() 方法。
stop() 方法會(huì)真的殺死線程宇姚,不給線程喘息的機(jī)會(huì)匈庭,如果線程持有 ReentrantLock 鎖,被 stop() 的線程并不會(huì)自動(dòng)調(diào)用 ReentrantLock 的 unlock() 去釋放鎖浑劳,那其他線程就再也沒(méi)機(jī)會(huì)獲得 ReentrantLock 鎖阱持。
interrupt() 方法僅僅是通知線程,線程有機(jī)會(huì)執(zhí)行一些后續(xù)操作魔熏,同時(shí)也可以無(wú)視這個(gè)通知衷咽。被 interrupt 的線程,是怎么收到通知的呢蒜绽?一種是異常镶骗,另一種是主動(dòng)檢測(cè)。
當(dāng)線程 A 處于 WAITING躲雅、TIMED_WAITING 狀態(tài)時(shí)鼎姊,如果其他線程調(diào)用線程 A 的 interrupt() 方法,會(huì)使線程 A 返回到 RUNNABLE 狀態(tài)相赁,同時(shí)線程 A 的代碼會(huì)觸發(fā) InterruptedException 異常相寇。上面我們提到轉(zhuǎn)換到 WAITING、TIMED_WAITING 狀態(tài)的觸發(fā)條件钮科,都是調(diào)用了類(lèi)似 wait()唤衫、join()、sleep() 這樣的方法跺嗽,我們看這些方法的簽名战授,發(fā)現(xiàn)都會(huì) throws InterruptedException 這個(gè)異常页藻。這個(gè)異常的觸發(fā)條件就是:其他線程調(diào)用了該線程的 interrupt() 方法。
6植兰、創(chuàng)建合理的線程數(shù)量
創(chuàng)建合理的線程數(shù)量份帐,目的是將硬件的性能發(fā)揮到極致。根據(jù)不同的應(yīng)用場(chǎng)景楣导,我們將應(yīng)用分為兩種場(chǎng)景論述废境,I/O密集型應(yīng)用和CPU密集型應(yīng)用。
CPU密集型應(yīng)用
對(duì)于 CPU 密集型的計(jì)算場(chǎng)景筒繁,理論上“線程的數(shù)量 =CPU 核數(shù)”就是最合適的噩凹。不過(guò)在工程上,線程的數(shù)量一般會(huì)設(shè)置為“CPU 核數(shù) +1”毡咏,這樣的話驮宴,當(dāng)線程因?yàn)榕紶柕膬?nèi)存頁(yè)失效或其他原因?qū)е伦枞麜r(shí),這個(gè)額外的線程可以頂上呕缭,從而保證 CPU 的利用率堵泽。
I/O密集型應(yīng)用
對(duì)于 I/O 密集型計(jì)算場(chǎng)景,最佳的線程數(shù)是與程序中 CPU 計(jì)算和 I/O 操作的耗時(shí)比相關(guān)的恢总,我們可以總結(jié)出這樣一個(gè)公式:
最佳線程數(shù) =1 +(I/O 耗時(shí) / CPU 耗時(shí))
不過(guò)上面這個(gè)公式是針對(duì)單核 CPU 的迎罗,至于多核 CPU,也很簡(jiǎn)單片仿,只需要等比擴(kuò)大就可以了纹安,計(jì)算公式如下:
最佳線程數(shù) =CPU 核數(shù) * [ 1 +(I/O 耗時(shí) / CPU 耗時(shí))]
Q: 有些同學(xué)對(duì)于最佳線程數(shù)的設(shè)置積累了一些經(jīng)驗(yàn)值,認(rèn)為對(duì)于 I/O 密集型應(yīng)用砂豌,最佳線程數(shù)應(yīng)該為:2 * CPU 的核數(shù) + 1厢岂,你覺(jué)得這個(gè)經(jīng)驗(yàn)值合理嗎?
A: 工作中都是按照邏輯核數(shù)來(lái)的奸鸯,理論值和經(jīng)驗(yàn)值只是提供個(gè)指導(dǎo)咪笑,實(shí)際上還是要靠壓測(cè)!Bι窗怒!
7、Lock與synchronized的不同
synchronized存在蓄拣,Javasdk還造一個(gè)Lock的原因主要是為了彌補(bǔ)synchronized扬虚,會(huì)阻塞線程且不會(huì)釋放已經(jīng)占有的資源的問(wèn)題,lock的三個(gè)方法如下:
// 支持響應(yīng)中斷
void lockInterruptibly()
throws InterruptedException;
// 支持超時(shí)
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞獲取鎖
boolean tryLock();
并且球恤,Lock支持多個(gè)條件變量辜昵。Lock用來(lái)實(shí)現(xiàn)“互斥”,condition用來(lái)實(shí)現(xiàn)“同步”咽斧。
用兩個(gè)條件變量實(shí)現(xiàn)阻塞隊(duì)列
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 條件變量:隊(duì)列不滿
final Condition notFull =
lock.newCondition();
// 條件變量:隊(duì)列不空
final Condition notEmpty =
lock.newCondition();
// 入隊(duì)
void enq(T x) {
lock.lock();
try {
while (隊(duì)列已滿){
// 等待隊(duì)列不滿
notFull.await();
}
// 省略入隊(duì)操作...
//入隊(duì)后,通知可出隊(duì)
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出隊(duì)
void deq(){
lock.lock();
try {
while (隊(duì)列已空){
// 等待隊(duì)列不空
notEmpty.await();
}
// 省略出隊(duì)操作...
//出隊(duì)后堪置,通知可入隊(duì)
notFull.signal();
}finally {
lock.unlock();
}
}
}
不過(guò)躬存,這里你需要注意,Lock 和 Condition 實(shí)現(xiàn)的管程舀锨,線程等待和通知需要調(diào)用 await()岭洲、signal()、signalAll()坎匿,它們的語(yǔ)義和 wait()盾剩、notify()、notifyAll() 是相同的替蔬。但是不一樣的是告私,Lock&Condition 實(shí)現(xiàn)的管程里只能使用前面的 await()、signal()承桥、signalAll()驻粟,而后面的 wait()、notify()快毛、notifyAll() 只有在 synchronized 實(shí)現(xiàn)的管程里才能使用格嗅。如果一不小心在 Lock&Condition 實(shí)現(xiàn)的管程里調(diào)用了 wait()番挺、notify()唠帝、notifyAll(),那程序可就徹底玩兒完了玄柏。
異步轉(zhuǎn)同步
遠(yuǎn)程調(diào)用rpc請(qǐng)求時(shí)襟衰,面臨異步轉(zhuǎn)同步的問(wèn)題,因?yàn)閠cp層面上rpc請(qǐng)求就是異步的粪摘,它不會(huì)等待請(qǐng)求返回結(jié)果瀑晒,所以類(lèi)似于dubbo這種rpc框架也是做了異步轉(zhuǎn)同步的工作,具體實(shí)現(xiàn)類(lèi)似于上面“用兩個(gè)條件變量實(shí)現(xiàn)阻塞隊(duì)列”的代碼徘意。
8苔悦、用Semaphore實(shí)現(xiàn)一個(gè)限流器
極客專(zhuān)欄鏈接 https://time.geekbang.org/column/article/88499
Semaphore是信號(hào)量的意思,可以允許多個(gè)線程訪問(wèn)一個(gè)臨界區(qū)椎咧【料辏可以用來(lái)實(shí)現(xiàn)比較常見(jiàn)的需求就是我們工作中遇到的各種池化資源,例如連接池勤讽、對(duì)象池蟋座、線程池等等。
9脚牍、讀寫(xiě)鎖 ReadWriteLock
極客專(zhuān)欄鏈接(質(zhì)量很高向臀,也很實(shí)用) https://time.geekbang.org/column/article/88909
這個(gè)鏈接里文章實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的完備緩存的示例。
讀寫(xiě)鎖升級(jí)問(wèn)題
讀鎖不能升級(jí)為寫(xiě)鎖诸狭。
寫(xiě)鎖可以降級(jí)為讀鎖券膀。
如果進(jìn)行讀寫(xiě)鎖升級(jí)君纫,讀鎖還沒(méi)有釋放,此時(shí)獲取寫(xiě)鎖芹彬,會(huì)導(dǎo)致寫(xiě)鎖永久等待庵芭,最終導(dǎo)致相關(guān)線程都被阻塞,永遠(yuǎn)也沒(méi)有機(jī)會(huì)被喚醒雀监。鎖的升級(jí)是不允許的双吆,這個(gè)你一定要注意。
9会前、StampedLock 比讀寫(xiě)鎖更快的鎖
極客專(zhuān)欄鏈接 使用stampedLock有幾個(gè)比較重要需要注意的點(diǎn)好乐,所以謹(jǐn)慎使用。
支持三種模式
寫(xiě)鎖
悲觀讀鎖(類(lèi)似與讀寫(xiě)鎖的讀鎖)
樂(lè)觀讀(無(wú)鎖瓦宜,檢測(cè)到有鎖的時(shí)候需要轉(zhuǎn)成悲觀讀鎖)
10蔚万、CountDownLatch 和 CyclicBarrier 讓多線程步調(diào)一致
極客時(shí)間專(zhuān)欄鏈接 https://time.geekbang.org/column/article/89461
CountDownLatch 和 CyclicBarrier 是 Java 并發(fā)包提供的兩個(gè)非常易用的線程同步工具類(lèi),這兩個(gè)工具類(lèi)用法的區(qū)別在這里還是有必要再?gòu)?qiáng)調(diào)一下:
CountDownLatch 主要用來(lái)解決一個(gè)線程等待多個(gè)線程的場(chǎng)景临庇,可以類(lèi)比旅游團(tuán)團(tuán)長(zhǎng)要等待所有的游客到齊才能去下一個(gè)景點(diǎn)反璃;
而 CyclicBarrier 是一組線程之間互相等待,更像是幾個(gè)驢友之間不離不棄假夺。
除此之外 CountDownLatch 的計(jì)數(shù)器是不能循環(huán)利用的淮蜈,也就是說(shuō)一旦計(jì)數(shù)器減到 0,再有線程調(diào)用 await()已卷,該線程會(huì)直接通過(guò)梧田。但 CyclicBarrier 的計(jì)數(shù)器是可以循環(huán)利用的,而且具備自動(dòng)重置的功能侧蘸,一旦計(jì)數(shù)器減到 0 會(huì)自動(dòng)重置到你設(shè)置的初始值裁眯。除此之外,CyclicBarrier 還可以設(shè)置回調(diào)函數(shù)讳癌,可以說(shuō)是功能豐富穿稳。
11、Java并發(fā)容器
通過(guò)對(duì)操作方法加synchronized關(guān)鍵字晌坤,可以使得一個(gè)容器變成線程安全的容器逢艘。這類(lèi)容器稱(chēng)為同步容器,針對(duì)同步容器的性能問(wèn)題泡仗,Java在1.5版本之后出來(lái)了并發(fā)容器埋虹,性能更高。
同步容器:
常見(jiàn)的有Vector娩怎、Stack 和 Hashtable等搔课,通過(guò)對(duì)操作方法加synchronized關(guān)鍵字實(shí)現(xiàn)。
并發(fā)容器:
數(shù)量較多,主要有以下四大類(lèi):Set爬泥、Map柬讨、Set、Queue
并發(fā)容器關(guān)系圖:
比較熟悉的有ConcurrentHashMap袍啡、BlockingQueue....
注意事項(xiàng)
1踩官、在容器領(lǐng)域一個(gè)容易被忽視的“坑”是用迭代器遍歷容器。
// 有問(wèn)題的寫(xiě)法
List list = Collections.
synchronizedList(new ArrayList());
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
// 正確的寫(xiě)法
// 因?yàn)槭菍?duì)list的操作境输,如果list變化了蔗牡,會(huì)使得迭代器報(bào)錯(cuò)
// 所以先把list鎖住,保證迭代器運(yùn)行期間list不會(huì)變化
// 這也是在迭代器里對(duì)當(dāng)前元素刪除會(huì)報(bào)錯(cuò)的原因
List list = Collections.
synchronizedList(new ArrayList());
Iterator i = list.iterator();
while (i.hasNext())
foo(i.next());
2嗅剖、另外辩越,使用隊(duì)列時(shí),需要格外注意隊(duì)列是否支持有界(所謂有界指的是內(nèi)部的隊(duì)列是否有容量限制)信粮。實(shí)際工作中黔攒,一般都不建議使用無(wú)界的隊(duì)列,因?yàn)閿?shù)據(jù)量大了之后很容易導(dǎo)致 OOM强缘。上面我們提到的這些 Queue 中督惰,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他無(wú)界隊(duì)列時(shí)旅掂,一定要充分考慮是否存在導(dǎo)致 OOM 的隱患赏胚。
12、原子類(lèi)
極客時(shí)間專(zhuān)欄 https://time.geekbang.org/column/article/90515
原子類(lèi)高性能的秘密就是硬件支持辞友,基于CPU提供的cas(compare and swap)指令栅哀。
ABA問(wèn)題
aba問(wèn)題,指的是一個(gè)共享變量經(jīng)歷了A--》B--》A的一個(gè)過(guò)程称龙,雖然最終值一樣,但是已經(jīng)被更新過(guò)了戳晌。使用原子化的更新對(duì)象很可能就需要關(guān)心 ABA 問(wèn)題鲫尊,因?yàn)閮蓚€(gè) A 雖然相等,但是第二個(gè) A 的屬性可能已經(jīng)發(fā)生變化了沦偎。
相關(guān)實(shí)現(xiàn)有 AtomicReference疫向、AtomicStampedReference 和 AtomicMarkableReference,利用它們可以實(shí)現(xiàn)對(duì)象引用的原子化更新豪嚎。AtomicReference 提供的方法和原子化的基本數(shù)據(jù)類(lèi)型差不多搔驼,這里不再贅述。不過(guò)需要注意的是侈询,對(duì)象引用的更新需要重點(diǎn)關(guān)注 ABA 問(wèn)題舌涨,AtomicStampedReference 和 AtomicMarkableReference 這兩個(gè)原子類(lèi)可以解決 ABA 問(wèn)題。
原子類(lèi)組成概覽
13扔字、Java中的線程池
美團(tuán)技術(shù)文章Java線程池
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
使用線程池的目的是為了避免線程的頻繁創(chuàng)建和銷(xiāo)毀囊嘉。線程是一個(gè)重量級(jí)的對(duì)象温技,應(yīng)該避免頻繁創(chuàng)建和銷(xiāo)毀。
線程池是一種生產(chǎn)者 - 消費(fèi)者模式扭粱。線程池的使用方是生產(chǎn)者舵鳞,線程池本身是消費(fèi)者。在下面的示例代碼中琢蛤,我們創(chuàng)建了一個(gè)非常簡(jiǎn)單的線程池 MyThreadPool蜓堕,你可以通過(guò)它來(lái)理解線程池的工作原理。
//簡(jiǎn)化的線程池博其,僅用來(lái)說(shuō)明工作原理
class MyThreadPool{
//利用阻塞隊(duì)列實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式
BlockingQueue<Runnable> workQueue;
//保存內(nèi)部工作線程
List<WorkerThread> threads
= new ArrayList<>();
// 構(gòu)造方法
MyThreadPool(int poolSize,
BlockingQueue<Runnable> workQueue){
this.workQueue = workQueue;
// 創(chuàng)建工作線程
for(int idx=0; idx<poolSize; idx++){
WorkerThread work = new WorkerThread();
work.start();
threads.add(work);
}
}
// 提交任務(wù)
void execute(Runnable command){
workQueue.put(command);
}
// 工作線程負(fù)責(zé)消費(fèi)任務(wù)俩滥,并執(zhí)行任務(wù)
class WorkerThread extends Thread{
public void run() {
//循環(huán)取任務(wù)并執(zhí)行
while(true){ ①
Runnable task = workQueue.take();
task.run();
}
}
}
}
/** 下面是使用示例 **/
// 創(chuàng)建有界阻塞隊(duì)列
BlockingQueue<Runnable> workQueue =
new LinkedBlockingQueue<>(2);
// 創(chuàng)建線程池
MyThreadPool pool = new MyThreadPool(
10, workQueue);
// 提交任務(wù)
pool.execute(()->{
System.out.println("hello");
});
在 MyThreadPool 的內(nèi)部,我們維護(hù)了一個(gè)阻塞隊(duì)列 workQueue 和一組工作線程贺奠,工作線程的個(gè)數(shù)由構(gòu)造函數(shù)中的 poolSize 來(lái)指定霜旧。用戶通過(guò)調(diào)用 execute() 方法來(lái)提交 Runnable 任務(wù),execute() 方法的內(nèi)部實(shí)現(xiàn)僅僅是將任務(wù)加入到 workQueue 中儡率。MyThreadPool 內(nèi)部維護(hù)的工作線程會(huì)消費(fèi) workQueue 中的任務(wù)并執(zhí)行任務(wù)挂据,相關(guān)的代碼就是代碼①處的 while 循環(huán)。線程池主要的工作原理就這些儿普,是不是還挺簡(jiǎn)單的崎逃?
// todo 線程的運(yùn)行過(guò)程
什么情況下增加新線程、添加到任務(wù)隊(duì)列等等
ThreadPoolExecutor 線程池參數(shù)
ThreadPoolExecutor(
int corePoolSize, // 表示線程池保有的最小線程數(shù)眉孩。
int maximumPoolSize, // 表示線程池創(chuàng)建的最大線程數(shù)个绍。當(dāng)項(xiàng)目很忙時(shí),就需要加人浪汪,最多加到 maximumPoolSize 個(gè)人巴柿。
long keepAliveTime, // 線程可以空閑的存活時(shí)間
TimeUnit unit,
BlockingQueue<Runnable> workQueue, //工作隊(duì)列,用來(lái)保存生產(chǎn)的資源死遭,也是線程要消費(fèi)的資源
ThreadFactory threadFactory, // 通過(guò)這個(gè)參數(shù)自定義如何創(chuàng)建線程广恢,如給線程指定一個(gè)名字
RejectedExecutionHandler handler) // 拒絕策略
拒絕策略
ThreadPoolExecutor 已經(jīng)提供了以下 4 種策略。
CallerRunsPolicy:提交任務(wù)的線程自己去執(zhí)行該任務(wù)呀潭。
AbortPolicy:默認(rèn)的拒絕策略钉迷,會(huì) throws RejectedExecutionException。
DiscardPolicy:直接丟棄任務(wù)钠署,沒(méi)有任何異常拋出糠聪。
DiscardOldestPolicy:丟棄最老的任務(wù),其實(shí)就是把最早進(jìn)入工作隊(duì)列的任務(wù)丟棄谐鼎,然后把新任務(wù)加入到工作隊(duì)列舰蟆。
使用線程池的注意事項(xiàng)
1、盡量使用有界隊(duì)列
Java 并發(fā)包里提供了一個(gè)線程池的靜態(tài)工廠類(lèi) Executors,利用 Executors 你可以快速創(chuàng)建線程池夭苗。不過(guò)目前大廠的編碼規(guī)范中基本上都不建議使用 Executors 了信卡。
不建議使用 Executors 的最重要的原因是:Executors 提供的很多方法默認(rèn)使用的都是無(wú)界的 LinkedBlockingQueue,高負(fù)載情境下题造,無(wú)界隊(duì)列很容易導(dǎo)致 OOM傍菇,而 OOM 會(huì)導(dǎo)致所有請(qǐng)求都無(wú)法處理,這是致命問(wèn)題界赔。所以強(qiáng)烈建議使用有界隊(duì)列丢习。
2、默認(rèn)的拒絕策略要慎用
使用有界隊(duì)列淮悼,當(dāng)任務(wù)過(guò)多時(shí)咐低,線程池會(huì)觸發(fā)執(zhí)行拒絕策略,線程池默認(rèn)的拒絕策略會(huì) throw RejectedExecutionException 這是個(gè)運(yùn)行時(shí)異常袜腥,對(duì)于運(yùn)行時(shí)異常編譯器并不強(qiáng)制 catch 它见擦,所以開(kāi)發(fā)人員很容易忽略。因此默認(rèn)拒絕策略要慎重使用羹令。
14鲤屡、Future 獲取異步執(zhí)行結(jié)果
極客時(shí)間專(zhuān)欄 https://time.geekbang.org/column/article/91292
如何獲取異步任務(wù)執(zhí)行結(jié)果
Java 通過(guò) ThreadPoolExecutor 提供的 3 個(gè) submit() 方法和 1 個(gè) FutureTask 工具類(lèi)來(lái)支持獲得任務(wù)執(zhí)行結(jié)果的需求。下面我們先來(lái)介紹這 3 個(gè) submit() 方法福侈,這 3 個(gè)方法的方法簽名如下酒来。
// 提交Runnable任務(wù)
Future<?>
submit(Runnable task);
// 提交Callable任務(wù)
<T> Future<T>
submit(Callable<T> task);
// 提交Runnable任務(wù)及結(jié)果引用
<T> Future<T>
submit(Runnable task, T result);
你會(huì)發(fā)現(xiàn)它們的返回值都是 Future 接口,F(xiàn)uture 接口有 5 個(gè)方法肪凛,我都列在下面了堰汉,它們分別是取消任務(wù)的方法 cancel()、判斷任務(wù)是否已取消的方法 isCancelled()伟墙、判斷任務(wù)是否已結(jié)束的方法 isDone()以及2 個(gè)獲得任務(wù)執(zhí)行結(jié)果的 get() 和 get(timeout, unit)翘鸭,其中最后一個(gè) get(timeout, unit) 支持超時(shí)機(jī)制。通過(guò) Future 接口的這 5 個(gè)方法你會(huì)發(fā)現(xiàn)远荠,我們提交的任務(wù)不但能夠獲取任務(wù)執(zhí)行結(jié)果矮固,還可以取消任務(wù)。不過(guò)需要注意的是:這兩個(gè) get() 方法都是阻塞式的譬淳,如果被調(diào)用的時(shí)候,任務(wù)還沒(méi)有執(zhí)行完盹兢,那么調(diào)用 get() 方法的線程會(huì)阻塞邻梆,直到任務(wù)執(zhí)行完才會(huì)被喚醒。
// 取消任務(wù)
boolean cancel(
boolean mayInterruptIfRunning);
// 判斷任務(wù)是否已取消
boolean isCancelled();
// 判斷任務(wù)是否已結(jié)束
boolean isDone();
// 獲得任務(wù)執(zhí)行結(jié)果
get();
// 獲得任務(wù)執(zhí)行結(jié)果绎秒,支持超時(shí)
get(long timeout, TimeUnit unit);
FutureTask工具類(lèi)
前面我們提到的 Future 是一個(gè)接口浦妄,而 FutureTask 是一個(gè)實(shí)實(shí)在在的工具類(lèi),這個(gè)工具類(lèi)有兩個(gè)構(gòu)造函數(shù),它們的參數(shù)和前面介紹的 submit() 方法類(lèi)似剂娄,所以這里我就不再贅述了蠢涝。
FutureTask(Callable<V> callable);
FutureTask(Runnable runnable, V result);
那如何使用 FutureTask 呢?其實(shí)很簡(jiǎn)單阅懦,F(xiàn)utureTask 實(shí)現(xiàn)了 Runnable 和 Future 接口和二,由于實(shí)現(xiàn)了 Runnable 接口,所以可以將 FutureTask 對(duì)象作為任務(wù)提交給 ThreadPoolExecutor 去執(zhí)行耳胎,也可以直接被 Thread 執(zhí)行惯吕;又因?yàn)閷?shí)現(xiàn)了 Future 接口,所以也能用來(lái)獲得任務(wù)的執(zhí)行結(jié)果怕午。下面的示例代碼是將 FutureTask 對(duì)象提交給 ThreadPoolExecutor 去執(zhí)行废登。
// 創(chuàng)建FutureTask
FutureTask<Integer> futureTask
= new FutureTask<>(()-> 1+2);
// 創(chuàng)建線程池
ExecutorService es =
Executors.newCachedThreadPool();
// 提交FutureTask
es.submit(futureTask);
// 獲取計(jì)算結(jié)果
Integer result = futureTask.get();
FutureTask 對(duì)象直接被 Thread 執(zhí)行的示例代碼如下所示。相信你已經(jīng)發(fā)現(xiàn)了郁惜,利用 FutureTask 對(duì)象可以很容易獲取子線程的執(zhí)行結(jié)果堡距。
// 創(chuàng)建FutureTask
FutureTask<Integer> futureTask
= new FutureTask<>(()-> 1+2);
// 創(chuàng)建并啟動(dòng)線程
Thread T1 = new Thread(futureTask);
T1.start();
// 獲取計(jì)算結(jié)果
Integer result = futureTask.get();
// 以上兩種方式還可以組合起來(lái)使用。
下面的示例代碼就是用這一章提到的 Future 特性來(lái)實(shí)現(xiàn)的兆蕉。首先羽戒,我們創(chuàng)建了兩個(gè) FutureTask——ft1 和 ft2,ft1 完成洗水壺恨樟、燒開(kāi)水半醉、泡茶的任務(wù),ft2 完成洗茶壺劝术、洗茶杯缩多、拿茶葉的任務(wù);這里需要注意的是 ft1 這個(gè)任務(wù)在執(zhí)行泡茶任務(wù)前养晋,需要等待 ft2 把茶葉拿來(lái)衬吆,所以 ft1 內(nèi)部需要引用 ft2,并在執(zhí)行泡茶之前绳泉,調(diào)用 ft2 的 get() 方法實(shí)現(xiàn)等待逊抡。
// 創(chuàng)建任務(wù)T2的FutureTask
FutureTask<String> ft2
= new FutureTask<>(new T2Task());
// 創(chuàng)建任務(wù)T1的FutureTask
FutureTask<String> ft1
= new FutureTask<>(new T1Task(ft2));
// 線程T1執(zhí)行任務(wù)ft1
Thread T1 = new Thread(ft1);
T1.start();
// 線程T2執(zhí)行任務(wù)ft2
Thread T2 = new Thread(ft2);
T2.start();
// 等待線程T1執(zhí)行結(jié)果
System.out.println(ft1.get());
// T1Task需要執(zhí)行的任務(wù):
// 洗水壺、燒開(kāi)水零酪、泡茶
class T1Task implements Callable<String>{
FutureTask<String> ft2;
// T1任務(wù)需要T2任務(wù)的FutureTask
T1Task(FutureTask<String> ft2){
this.ft2 = ft2;
}
@Override
String call() throws Exception {
System.out.println("T1:洗水壺...");
TimeUnit.SECONDS.sleep(1);
System.out.println("T1:燒開(kāi)水...");
TimeUnit.SECONDS.sleep(15);
// 獲取T2線程的茶葉
String tf = ft2.get();
System.out.println("T1:拿到茶葉:"+tf);
System.out.println("T1:泡茶...");
return "上茶:" + tf;
}
}
// T2Task需要執(zhí)行的任務(wù):
// 洗茶壺冒嫡、洗茶杯、拿茶葉
class T2Task implements Callable<String> {
@Override
String call() throws Exception {
System.out.println("T2:洗茶壺...");
TimeUnit.SECONDS.sleep(1);
System.out.println("T2:洗茶杯...");
TimeUnit.SECONDS.sleep(2);
System.out.println("T2:拿茶葉...");
TimeUnit.SECONDS.sleep(1);
return "龍井";
}
}
// 一次執(zhí)行結(jié)果:
T1:洗水壺...
T2:洗茶壺...
T1:燒開(kāi)水...
T2:洗茶杯...
T2:拿茶葉...
T1:拿到茶葉:龍井
T1:泡茶...
上茶:龍井
14四苇、CompletableFuture 異步編程
極客時(shí)間專(zhuān)欄文章鏈接 https://time.geekbang.org/column/article/91569
CompletableFuture 實(shí)現(xiàn)了 CompletionStage 接口孝凌。任務(wù)是有時(shí)序關(guān)系的,比如有串行關(guān)系月腋、并行關(guān)系蟀架、匯聚關(guān)系等瓣赂,這些都在CompletionStage接口中得到了體現(xiàn)。很好用就是了片拍。具體參考上述文章鏈接煌集。
注意異常處理,默認(rèn)情況下 CompletableFuture 會(huì)使用公共的 ForkJoinPool 線程池捌省,這個(gè)線程池默認(rèn)創(chuàng)建的線程數(shù)是 CPU 的核數(shù)(也可以通過(guò) JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 來(lái)設(shè)置 ForkJoinPool 線程池的線程數(shù))苫纤。如果所有 CompletableFuture 共享一個(gè)線程池,那么一旦有任務(wù)執(zhí)行一些很慢的 I/O 操作所禀,就會(huì)導(dǎo)致線程池中所有線程都阻塞在 I/O 操作上方面,從而造成線程饑餓,進(jìn)而影響整個(gè)系統(tǒng)的性能色徘。所以恭金,強(qiáng)烈建議你要根據(jù)不同的業(yè)務(wù)類(lèi)型創(chuàng)建不同的線程池,以避免互相干擾褂策。
15横腿、CompletionService 批量執(zhí)行異步任務(wù)
當(dāng)需要批量提交異步任務(wù)的時(shí)候建議你使用 CompletionService。CompletionService 將線程池 Executor 和阻塞隊(duì)列 BlockingQueue 的功能融合在了一起斤寂,能夠讓批量異步任務(wù)的管理更簡(jiǎn)單耿焊。除此之外,CompletionService 能夠讓異步任務(wù)的執(zhí)行結(jié)果有序化遍搞,先執(zhí)行完的先進(jìn)入阻塞隊(duì)列罗侯,利用這個(gè)特性,你可以輕松實(shí)現(xiàn)后續(xù)處理的有序性溪猿,避免無(wú)謂的等待钩杰,同時(shí)還可以快速實(shí)現(xiàn)諸如 Forking Cluster 這樣的需求。CompletionService 的實(shí)現(xiàn)類(lèi) ExecutorCompletionService诊县,需要你自己創(chuàng)建線程池讲弄,雖看上去有些啰嗦,但好處是你可以讓多個(gè) ExecutorCompletionService 的線程池隔離依痊,這種隔離性能避免幾個(gè)特別耗時(shí)的任務(wù)拖垮整個(gè)應(yīng)用的風(fēng)險(xiǎn)避除。
16、Fork/Join 并行計(jì)算框架
極客時(shí)間專(zhuān)欄文章鏈接 https://time.geekbang.org/column/article/92524
Fork/Join 并行計(jì)算框架主要解決的是分治任務(wù)胸嘁。分治的核心思想是“分而治之”:將一個(gè)大的任務(wù)拆分成小的子任務(wù)去解決瓶摆,然后再把子任務(wù)的結(jié)果聚合起來(lái)從而得到最終結(jié)果。這個(gè)過(guò)程非常類(lèi)似于大數(shù)據(jù)處理中的 MapReduce性宏,所以你可以把 Fork/Join 看作單機(jī)版的 MapReduce赏壹。
Fork/Join 并行計(jì)算框架的核心組件是 ForkJoinPool。ForkJoinPool 支持任務(wù)竊取機(jī)制衔沼,能夠讓所有線程的工作量基本均衡蝌借,不會(huì)出現(xiàn)有的線程很忙,而有的線程很閑的狀況指蚁,所以性能很好菩佑。Java 1.8 提供的 Stream API 里面并行流也是以 ForkJoinPool 為基礎(chǔ)的。不過(guò)需要你注意的是凝化,默認(rèn)情況下所有的并行流計(jì)算都共享一個(gè) ForkJoinPool稍坯,這個(gè)共享的 ForkJoinPool 默認(rèn)的線程數(shù)是 CPU 的核數(shù);如果所有的并行流計(jì)算都是 CPU 密集型計(jì)算的話搓劫,完全沒(méi)有問(wèn)題瞧哟,但是如果存在 I/O 密集型的并行流計(jì)算,那么很可能會(huì)因?yàn)橐粋€(gè)很慢的 I/O 計(jì)算而拖慢整個(gè)系統(tǒng)的性能枪向。所以建議用不同的 ForkJoinPool 執(zhí)行不同類(lèi)型的計(jì)算任務(wù)勤揩。
模擬MapReduce統(tǒng)計(jì)單詞數(shù)量
學(xué)習(xí) MapReduce 有一個(gè)入門(mén)程序,統(tǒng)計(jì)一個(gè)文件里面每個(gè)單詞的數(shù)量秘蛔,下面我們來(lái)看看如何用 Fork/Join 并行計(jì)算框架來(lái)實(shí)現(xiàn)陨亡。我們可以先用二分法遞歸地將一個(gè)文件拆分成更小的文件,直到文件里只有一行數(shù)據(jù)深员,然后統(tǒng)計(jì)這一行數(shù)據(jù)里單詞的數(shù)量负蠕,最后再逐級(jí)匯總結(jié)果,你可以對(duì)照前面的簡(jiǎn)版分治任務(wù)模型圖來(lái)理解這個(gè)過(guò)程倦畅。
上述極客時(shí)間鏈接里存在具體的實(shí)現(xiàn)代碼遮糖,可以仔細(xì)看看。