Java并發(fā)編程之美
閱讀開源框架的一點心得
為什么要看源碼
-
由經(jīng)驗不足導(dǎo)致的問題
- 不知道如何去設(shè)計咏瑟,就看當(dāng)前系統(tǒng)類似需求的設(shè)計拂到,然后去仿照
- 設(shè)計的時候,考慮不周全
工作經(jīng)驗的積累來自于年限與實踐码泞,看源碼可以擴展思路
-
可以解決經(jīng)驗不足的辦法
-
通過學(xué)習(xí)開源框架兄旬、開源項目來獲取經(jīng)驗
- 對于比較大型的項目,可能迷失在源碼中,需要找到自己想要的點
-
-
看源碼的好處
如果有閱讀源碼的經(jīng)驗领铐,研究新系統(tǒng)的代碼邏輯時就不會那么費勁
當(dāng)你使用框架或者工具做開發(fā)時悯森,如果對它的實現(xiàn)有所了解,就能最大化的減少出故障的可能
解決比較難的問題
快速解決問題
-
開闊思維绪撵,提升架構(gòu)設(shè)計能力
- 有些東西靠書本和自己思考是很難學(xué)到的瓢姻,必須通過看源碼,看別人如何設(shè)計音诈,然后思考為何這樣設(shè)計才能領(lǐng)悟到
打發(fā)時間
-
能力的提高
-
不在于寫了多少代碼幻碱,做了多少項目,而在于給一個業(yè)務(wù)場景時细溅,能拿出幾種靠譜的解決方案褥傍,并且說出各自的優(yōu)缺點
- 靠經(jīng)驗和歸納總結(jié)
- 看源碼可以快速增加經(jīng)驗
-
如何看源碼
在看某個框架的源碼前,先去Google查找這個開源框架的官方介紹喇聊,通過資料了解該框架有幾個模塊恍风,各個模塊是做什么的,之間有什么聯(lián)系誓篱,每個模塊都有哪些核心類朋贬,在閱讀源碼時可以著重看這些類。
-
對哪個模塊感興趣就寫demo窜骄,了解這個模塊的具體作用兄世,然后再debug進入看具體實現(xiàn)
- 在debug的過程中,第一遍是走馬觀花啊研,簡略看一下調(diào)用邏輯御滩,都用了哪些類;第二遍需有重點的debug党远,看看這些類擔(dān)任了架構(gòu)圖例的哪些功能削解,使用了哪些設(shè)計模式。第三遍debug沟娱,最好把主要類的調(diào)用時序圖以及類圖結(jié)構(gòu)畫出來氛驮,等畫好后,再對時序圖分析調(diào)用流程济似,知道類之間的調(diào)用關(guān)系矫废,通過類圖可以知道類的功能以及它們之間的依賴關(guān)系
-
閱讀開源框架里每個類或者方法上的注釋
- JUC包里的一些并發(fā)組件的注釋,就已經(jīng)說明了它們的設(shè)計原理和使用場景
在閱讀源碼時砰蠢,最好畫出時序圖和類圖蓖扑,因為人總是善忘的。避免每次從頭開始debug
查框架使用說明最好去官網(wǎng)查
研究代碼時台舱,不一定非要debug三遍律杠,這只是三種掌握程度,如果debug一遍就能掌握,那自然好
自己寫簡易
看源碼相關(guān)的坑
項目基本停止維護柜去,官網(wǎng)不更新
-
項目過于小眾灰嫉,包括國內(nèi)使用比較少、項目比較新嗓奢,網(wǎng)上資料很少
- 比如當(dāng)時的Laravel
官方文檔讼撒、博客與實際項目不符,版本不一致
開源項目質(zhì)量良莠不齊
什么源碼比較適合看
- 經(jīng)典的開源項目股耽,比如spring椿肩、netty、jdk豺谈、可供參考的開源項目
Java并發(fā)編程基礎(chǔ)篇
并發(fā)編程線程基礎(chǔ)
-
什么是線程
進程是系統(tǒng)進行資源分配和調(diào)度的基本單位郑象,線程則是進程的一個執(zhí)行路徑,一個進程中至少有一個線程茬末,進程中的多個新城共享進程的資源
CPU是比較特殊的資源厂榛,它是被分配到線程的,因為真正要占用CPU運行的是線程丽惭。線程是CPU分配的基本單位
在Java中击奶,當(dāng)啟動main函數(shù)時,其實就啟動了一個JVM的進程责掏,而main函數(shù)所在的線程就是這個進程中的一個線程柜砾,稱為主線程
-
進程和線程的關(guān)系
- 一個進程中有多個線程,多個線程共享進程的堆和方法區(qū)資源换衬,但是每個線程有自己的程序計數(shù)器和棧區(qū)域
- 程序計數(shù)器是一塊內(nèi)存區(qū)域痰驱,用來記錄線程當(dāng)前要執(zhí)行的指令地址
- 之所以設(shè)置程序計數(shù)器為私有,是為了在CPU輪詢時保存程序執(zhí)行地址
- 如果執(zhí)行的是native方法瞳浦,那么pc計數(shù)記錄的是undefined地址担映,只有執(zhí)行的是Java代碼時pc計數(shù)器記錄的才是下一條指令的地址
- 線程自己的棧資源,用于存儲該線程的局部變量叫潦,這些局部變量是該線程私有的蝇完,其他線程是訪問不了的,棧還用來存放線程的調(diào)用棧幀
- 堆是一個進程中最大的一塊內(nèi)存矗蕊,堆是被進程中的所有線程共享的短蜕,是進程創(chuàng)建時分配的,堆里面主要存放使用new操作創(chuàng)建的對象實例
- 方法區(qū)則用來存放JVM加載的類傻咖、常量及靜態(tài)變量等信息朋魔,也是線程共享的
-
線程創(chuàng)建與運行
-
Java中有三種線程創(chuàng)建方式
-
實現(xiàn)Runnable接口的run方法
- 實現(xiàn)Runnable接口可以繼承其他類,缺點是沒有返回值
-
繼承Thread類并重寫run的方法
- 好處是在run()方法內(nèi)獲取當(dāng)前線程直接使用this就可以了没龙,無需使用Thread.currentThread()方法铺厨;缺點是Java不支持多繼承,如果繼承了Thread類硬纤,那么就不能繼承其他類解滓;任務(wù)域代碼沒有分離,當(dāng)多個線程執(zhí)行一樣的任務(wù)時需要多分任務(wù)代碼筝家,而Runnable則沒有這個限制洼裤,沒有返回值
使用FutureTask方式
這里需要一個表格來比較各個創(chuàng)建方式的優(yōu)缺點
線程的狀態(tài)轉(zhuǎn)換圖是啥?條件是啥溪王?怎么查看線程的狀態(tài)腮鞍?終止了還能再運行嗎?
-
當(dāng)創(chuàng)建完thread對象后該線程并沒有被啟動執(zhí)行莹菱,直到調(diào)用了start方法后才真正啟動了線程
調(diào)用start方法后吸納還曾并沒有馬上執(zhí)行而是處于就緒狀態(tài)移国,這個就緒狀態(tài)是指該線程已經(jīng)獲取了除CPU資源外的其他資源,等待獲取CPU資源后才會處于運行狀態(tài)道伟。一旦run方法執(zhí)行完畢迹缀,該線程就處于終止?fàn)顟B(tài)
-
-
線程通知與等待
-
Java中的Object類是所有類的父類,Java把所有類都需要的方法放到了Object類里面蜜徽,包括通知與等待系列函數(shù)
- Object類有哪些方法祝懂?
- 為什么把等待、通知放到Object類中?
-
等待與通知系列函數(shù)
-
wait()函數(shù)
-
當(dāng)一個線程調(diào)用一個共享變量的wait()方法時拘鞋,該調(diào)用線程會被阻塞掛起砚蓬,直到發(fā)生幾件事才返回
- 其他線程調(diào)用了該共享對象的notify()或者notifyAll()方法
- 其他線程調(diào)用了該線程的interrupt()方法,該線程拋出InterruptedException異常返回
如果調(diào)用wait()方法的線程沒有事先獲取該對象的監(jiān)視器鎖盆色,則調(diào)用wait()方法時調(diào)用線程會拋出IllegalMonitorStateException異常
-
一個線程獲取一個共享變量的監(jiān)視器鎖的方式
-
執(zhí)行synchronized同步代碼塊時灰蛙,使用該共享變量作為參數(shù)
- synchronized(共享變量){}
-
調(diào)用該共享變量的方法,并且該方法使用了synchronized修飾
- synchronized void add(int a, int b){}
-
-
什么是共享變量隔躲?
- 如果要給變量再多個線程中都使用到了缕允,那么這個變量就是這幾個線程的共享變量
-
一個線程可以從掛起狀態(tài)變?yōu)榭梢赃\行狀態(tài)(喚醒),即使該線程沒有被其他線程notify()蹭越、notifyAll()方法進行通知障本,或者被中斷,或者等待超時响鹃,這就是所謂的虛假喚醒
-
雖然虛假喚醒在應(yīng)用實踐中很少發(fā)生驾霜,但要防患于未然,做發(fā)表就是不停區(qū)測試該線程被喚醒的條件是否滿足买置,不滿足則繼續(xù)等待粪糙,就是說在一個循環(huán)中調(diào)用wait()方法進行防范。退出循環(huán)的條件是滿足喚醒該線程的條件
-
生產(chǎn)者-消費者演示
- 生產(chǎn)者
- 消費者
自己默寫還是寫不出來
-
-
執(zhí)行wait()會釋放持有的鎖
-
-
wait(long)
- 如果要給線程調(diào)用共享對象的該方法掛起后忿项,沒有在指定的timeout ms實踐內(nèi)被其他線程調(diào)用該共享變量的notify()或者notifyAll()喚醒后蓉冈,那么該函數(shù)還是會因為超時而返回城舞。如果將timeout設(shè)置為0則和wait方法效果一樣。傳遞一個負的寞酿,會拋出異常
wait(long, int)
-
notify()函數(shù)
- 一個線程調(diào)用共享對象的notify()方法后家夺,會喚醒一個在該共享變量上調(diào)用wait系列方法后被掛起的線程。一個共享變量上可能會有多個線程在等待伐弹,具體喚醒哪個等待的線程是隨機的
- 只有當(dāng)前線程獲取到了共享變量的監(jiān)視器鎖后拉馋,才可以調(diào)用共享變量的noitfy方法,否則拋出異常
-
notifyAll()函數(shù)
- 喚醒所有在該共享變量上由于調(diào)用wait系列方法而被掛起的線程
-
-
-
等待線程執(zhí)行終止的join方法
- 主線程調(diào)用threadOne.join()方法后被阻塞惨好,等待threaOne執(zhí)行完畢后返回
- 線程A調(diào)用線程B的join方法后會被阻塞煌茴,當(dāng)其他線程調(diào)用了線程A的interrupt()方法中斷線程A時,線程A會拋出InterrutedException異常而返回
- CountDownLatch是不錯的選擇
-
讓線程睡眠的sleep方法
- Thread類中有一個靜態(tài)的sleep方法日川,當(dāng)一個執(zhí)行中的線程調(diào)用了Thread的sleep方法后蔓腐,調(diào)用線程會暫時讓出指定時間的執(zhí)行權(quán),即不參與CPU的調(diào)度龄句,但是該線程所擁有的監(jiān)視器資源合住,比如鎖,還是持有不讓出的
- 指定的睡眠時間到了后該函數(shù)會正常返回撒璧,線程就處于就緒狀態(tài)透葛,然后參與CPU的調(diào)度,獲取到CPU資源后就可以繼續(xù)運行了
- 如果在睡眠期間其他線程調(diào)用了該線程的interrupt()方法中斷了該線程卿樱,則該線程會在調(diào)用sleep方法的地方拋出InterruptedException異常而返回
-
讓出CPU執(zhí)行權(quán)的yield方法
- Thread類中有一個靜態(tài)的yield方法僚害,當(dāng)一個線程調(diào)用yield方法時,實際就是在暗示線程調(diào)度器當(dāng)前線程請求讓出自己的CPU使用繁调,但是線程調(diào)度器可以無條件忽略這個暗示
- 當(dāng)一個線程調(diào)用yield方法時萨蚕,當(dāng)前線程會讓出CPU使用權(quán),然后處于就緒狀態(tài)
- 一般很少使用這個方法蹄胰,在調(diào)式或者測試時這個方法或許可以幫助復(fù)現(xiàn)由于并發(fā)競爭條件導(dǎo)致的問題岳遥,在設(shè)計并發(fā)控制時或許有用途
- sleep與yield方法的區(qū)別在于,當(dāng)線程調(diào)用sleep方法時調(diào)用線程會被阻塞掛起指定時間裕寨,在這期間線程調(diào)度器不會區(qū)調(diào)度該線程浩蓉。而調(diào)用yield方法時,線程只是讓出自己剩余的時間片宾袜,并沒有被阻塞掛起捻艳,而是處于就緒狀態(tài)
-
線程中斷
Java中線程中斷,通過設(shè)置線程的中斷標(biāo)志并不能直接終止該線程的執(zhí)行庆猫,而是被中斷的線程根據(jù)中斷狀態(tài)自行處理
-
幾個方法
-
void interrupt()
- 中斷線程认轨。當(dāng)線程A運行時,線程B可以調(diào)用線程A的interrupt()方法來設(shè)置線程A的中斷標(biāo)志為true并立即返回月培。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志嘁字,線程A實際并沒有被中斷恩急,它會繼續(xù)往下執(zhí)行。
-
boolean isInterrupted()
- 檢查當(dāng)前線程是否被中斷纪蜒,如果是返回true衷恭,否則返回false
-
boolean interrupted()
- 檢查當(dāng)前線程是否被中斷,如果是返回true霍掺,否則返回false匾荆。與isInterrupted不同的是拌蜘,該方法如果發(fā)現(xiàn)當(dāng)前線程被中斷杆烁,則會清除中斷標(biāo)志,并且該方法是static方法简卧,可以通過Thread類直接調(diào)用
-
如何響應(yīng)中斷
-
理解線程上下文切換
什么是上下文切換
-
上下文切換時機
- 當(dāng)前線程的CPU時間片使用完處于就緒狀態(tài)時
- 當(dāng)前線程被其他線程中斷時
-
線程死鎖
-
什么是線程死鎖
死鎖是指兩個或兩個以上線程在執(zhí)行過程中兔魂,因爭奪資源而造成的互相等待的現(xiàn)象,在無外力作用的情況下举娩,這些線程會一直相互等待而無法繼續(xù)運行下去
死鎖的分類
-
死鎖產(chǎn)生必備的四個條件
-
互斥條件
- 指線程堆已獲取到的資源進行排他性使用析校,即該資源同時只由一個線程占用。如果此時還有其他線程請求獲取該資源铜涉,則請求者只能等待智玻,直至占有資源的線程釋放該資源
-
請求并持有條件
-
指一個線程已經(jīng)持有了至少一個資源,但又提出了新的資源請求芙代,而新資源已被其他線程占有吊奢,所以當(dāng)前線程會被阻塞,但阻塞的同時并部釋放自己已經(jīng)獲取的資源
- 就是至少爭奪兩個資源
-
-
不可剝奪條件
- 指線程獲取到的資源在自己使用完之前不能被其他線程搶占纹烹,只有在自己使用完畢后才由自己釋放該資源
-
環(huán)路等待條件
- 指在發(fā)生死鎖時页滚,必然存在一個線程一資源的環(huán)形鏈
-
死鎖演示代碼
一般發(fā)生死鎖的場景
-
如何避免線程死鎖
只需要破壞掉至少一個構(gòu)造死鎖的必要條件即可,目前只有請求并持有和環(huán)路等待條件是可以被破壞的
-
造成死鎖的原因和申請資源的順序有很大關(guān)系铺呵,使用資源申請的有序性原則就可以避免死鎖
- 相同的申請順序
-
守護線程與用戶線程
Java中線程分為兩類裹驰,分別為daemon線程和user線程
-
區(qū)別
- 當(dāng)沒有非守護線程時,JVM會正常退出片挂,而不管當(dāng)前是否有守護線程幻林,即守護線程是否結(jié)束并不影響JVM的退出
-
-
ThreadLocal
-
作用
- 提供線程本地變量,就是如果創(chuàng)建一個ThreadLocal變量音念,那么訪問這個變量的每個線程都會有這個變量的一個本地副本滋将。當(dāng)多個線程操作這個變量時,實際操作的是自己本地內(nèi)存里面的變量症昏,從而避免了線程安全問題
使用示例
-
實現(xiàn)原理
- 其實每個線程的本地變量不是存放在ThreadLocal實例里面随闽,而是存放在調(diào)用線程的threadLocals變量里面。ThreadLocal就是一個工具殼肝谭,他通過set方法把value值放入調(diào)用線程的threadLocals里面并存放起來掘宪,當(dāng)調(diào)用線程調(diào)用它的get方法時蛾扇,再從當(dāng)前線程的threadLocals變量里面將其拿出來使用
不支持繼承性
-
InheritableThreadLocal類
- 讓子線程可以訪問再父線程中設(shè)置的本地變量
-
并發(fā)編程的其他基礎(chǔ)知識
什么是多線程并發(fā)編程
為什么要進行多線程并發(fā)編程
-
Java中的線程安全問題
- 共享資源就是被多個線程訪問的資源
- 線程安全問題是指多個線程同時讀寫一個共享資源,并且沒有任何同步措施時魏滚,導(dǎo)致出現(xiàn)臟數(shù)據(jù)或者其他不可預(yù)見的結(jié)果問題
-
Java中共享變量的內(nèi)存可見性問題
-
Java內(nèi)存模型
- 子主題 1
- Java內(nèi)存模型歸檔镀首,將所有的變量都存放在主內(nèi)存中,當(dāng)線程使用變量時鼠次,會把主內(nèi)存里面的變量復(fù)制到自己的工作內(nèi)存更哄,線程讀寫變量時操作的是自己工作內(nèi)存中的變量
- 實際實現(xiàn)中的工作內(nèi)存
- 內(nèi)存可見性問題
-
-
Java中的synchronized關(guān)鍵字
-
介紹
- synchronized塊是Java提供的一種原子性內(nèi)置鎖,Java中的每個對象都可以作為要給同步鎖來使用腥寇。
- 線程的執(zhí)行代碼再進入synchronized代碼塊前會自動獲取內(nèi)部鎖成翩,這時候其他線程訪問該同步代碼塊時被阻塞掛起
- 拿到內(nèi)部鎖的線程會再正常退出同步代碼塊或者拋出異常后或者再同步塊內(nèi)調(diào)用了該內(nèi)置鎖資源的額wait系列方法時釋放該內(nèi)置鎖
- 內(nèi)置鎖是排他鎖
- synchronized的使用會導(dǎo)致上下文切換。因為阻塞一個線程時赦役,需要從用戶態(tài)切換到內(nèi)核態(tài)執(zhí)行阻塞操作麻敌,這是很耗時的操作
- 關(guān)鍵詞:內(nèi)置鎖、阻塞掂摔、獲取釋放鎖 术羔、排他鎖、上下文切換
-
內(nèi)存語義
- 進入synchronized塊的內(nèi)存語義是把在synchronized塊內(nèi)使用到的變量從線程的工作內(nèi)存中清除乙漓,這樣在synchronized塊內(nèi)使用到該變量時就不會從線程的工作內(nèi)存中獲取级历,而是直接從主內(nèi)存中獲取
- 退出synchronized塊的內(nèi)存語義是把在synchronized塊內(nèi)堆共享變量的修改刷新到主內(nèi)存
-
其實也是加鎖和釋放鎖的語義
- 當(dāng)獲取鎖后會清空鎖塊內(nèi)本地內(nèi)存中將會被用到的共享變量,在使用這些共享變量時從主內(nèi)存進行加載叭披,在釋放鎖時將本地內(nèi)存中修改的共享變量刷新到主內(nèi)存
-
用途
- 解決共享變量內(nèi)存可見性問題
- 原子性操作
- 有序性
-
注意
- synchronized關(guān)鍵字因為阻塞會引起線程上下文切換寥殖,并帶來線程調(diào)度開銷
-
-
Java中的volatile關(guān)鍵字
引入的背景是在解決共享內(nèi)存可見性使用鎖太笨重,因為它會帶來線程上下文的切換開銷
當(dāng)一個變量被聲明為volatile時趋观,線程在寫入變量時不會把值緩存在寄存器或者其他地方扛禽,而是會把值刷新回主存。當(dāng)其他線程讀取該共享變量時皱坛,會從主存重新獲取最新值编曼,而不是使用當(dāng)前線程的工作內(nèi)存中的值
-
內(nèi)存語義
- 當(dāng)線程寫了volatile變量值時等價于線程退出synchronized同步代碼塊(把寫入工作內(nèi)存的變量值同步到主內(nèi)存),讀取volatile變量值時就相當(dāng)于進入同步代碼塊(先清空本地內(nèi)存變量值剩辟,再從主內(nèi)存獲取最新值)
-
作用
- 提供可見性
-
使用場景
- 寫入變量值不依賴變量的當(dāng)前值時掐场。因為如果依賴當(dāng)前值,將是獲取-計算-寫入散步操作贩猎,這三部操作不是原子性的熊户,而volatile不保證原子性
- 讀寫變量值時沒有加鎖。因為加鎖本身已經(jīng)保證了內(nèi)存可見性吭服,這時候不需要把變量聲明為volatie
-
Java中的原子性操作
原子性操作是指執(zhí)行一系列操作時嚷堡,這些操作要么全部執(zhí)行,要么全部不執(zhí)行艇棕,不存在只執(zhí)行其中一部分的情況
通過javap -c查看匯編代碼蝌戒,來知道某操作是不是原子性的
-
保證原子性的方法
-
synchronized關(guān)鍵字
- 是獨占鎖
- 對于讀操作串塑,是為了保證多線程的可見性
原子性操作類AtomicLong之類的
-
-
Java中的CAS操作
使用鎖不好的地方,就是當(dāng)一個線程沒有獲取到鎖時會被阻塞掛起北苟,這會導(dǎo)致線程上下文的切換和重新調(diào)度開銷
非阻塞volatile關(guān)鍵字解決共享變量的可見性問題桩匪,一定程度上彌補了鎖帶來的開銷問題,但是volatile只能保證共享變量的可見性友鼻,不能解決讀-寫-寫等原子性問題
-
Compare and Swap
- 它是JDK提供的非阻塞原子性操作傻昙,它通過硬件保證了比較-更新操作的原子性。
- JDK里面的Unsafe類提供了一系列的compareAndSwap*方法
-
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)
- 四個參數(shù)彩扔,分別時對象內(nèi)存位置妆档、對象中變量的便宜量、變量預(yù)期值和新的值
- 含義是借杰,如果對象obj中內(nèi)存偏移量為valueOffset的變量值為expect过吻,則使用新的值update替換舊的值expect进泼。這是處理器提供的一個原子性指令
-
ABA問題
- 因為變量的狀態(tài)值產(chǎn)生了喚醒轉(zhuǎn)換蔗衡,就是變量的值從A到B,然后再從B到A
- AtomicStamepdReference類給每個變量的狀態(tài)值都配備了一個時間戳乳绕,從而避免了ABA問題的產(chǎn)生
-
Unsafe類
-
重要方法
JDK的rt.jar包中的Unsafe類提供了硬件級別的原子性操作绞惦,Unsafe類中的方法都是native方法,它們使用JNI的方式訪問本地C++實現(xiàn)庫
-
幾個主要的方法
- long objectFieldOffset(Field filed):返回指定的變量在所屬類中的內(nèi)存偏移地址洋措,該偏移地址僅僅在該Unsafe函數(shù)中訪問指定字段時使用
- int arrayBaseOffset(Class arrayClass):獲取數(shù)組中第一個元素的地址
- int arrayIndexScale(Class arrayClass):獲取數(shù)組中一個元素占用的字節(jié)
- boolean compareAndSwapLong(Object obj, long offset, long expect, long update):比較對象obj中偏移量為offset的變量的值是否與expect相等济蝉,相等則使用update值更新,然后返回true菠发,否則返回false
- public native long getLongvolatile(Object obj, long offset):獲取對象obj中偏移量為offset的變量對應(yīng)volatile語義的值
- void putLongvolatile(Object obj, long offset, long value):設(shè)置obj對象中offset偏移類型為long的field的值為value王滤,支持volatile語義
- void putOrderedLong(Object obj, long offset, long value):設(shè)置obj對象中offset便宜地址對應(yīng)的long型field的值為value。這是一個有延遲的putLongvolatile方法滓鸠,并且不保證值修改堆其他線程立刻可見雁乡。只有在變量使用volatile修飾并且預(yù)計會被意外修改時才使用該方法
- void park(boolean isAbsolute, long time):阻塞當(dāng)前線程,其中參數(shù)isAbsolute等于false且time等于0表示一直阻塞糜俗。time大于0表示等待指定的time后阻塞線程會被喚醒踱稍,這個time是相對值,是個增量值悠抹,就是相當(dāng)于當(dāng)前時間類加time后當(dāng)前線程就會被喚醒珠月。如果isAbsolute等價于true,并且time大于0楔敌,則表示阻塞的線程到指定的時間點會被喚醒啤挎,這里time是個絕對時間,是將某個時間點換算為ms后的值卵凑。
- 等等
如何使用Unsafe類
-
-
Java指令重排序
Java內(nèi)存模型允許編譯器和處理器對指令重排序以提高運行性能庆聘,并且只會對不存在數(shù)據(jù)依賴性的指令重排序旺韭。在單線程下重排序可以保證最終執(zhí)行的結(jié)果與程序順序執(zhí)行的結(jié)果一致,但是在多線程下就會存在問題
重排序是針對的單一線程內(nèi)執(zhí)行的指令重排序掏觉,多線程之間不存在
-
volatile修飾的變量具有避免重排序問題
- 寫volatile變量時区端,可以確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。讀volatile變量時澳腹,可以確保volatile讀之后的操作不會被編譯器重排序volatile讀之前
-
偽共享
- 什么是為共享
- 為何出現(xiàn)為共享
- 如何避免偽共享
- 小結(jié)
-
鎖的概述
-
樂觀鎖與悲觀鎖
- 樂觀鎖和悲觀鎖是在數(shù)據(jù)庫中引入的名詞织盼,但在并發(fā)包鎖里面也引入了類似思想
- 悲觀鎖指對數(shù)據(jù)被外界修改保持保守態(tài)度,認為數(shù)據(jù)很容易就會被其他線程修改酱塔,所以在數(shù)據(jù)被處理前先對數(shù)據(jù)進行加鎖沥邻,并在整個數(shù)據(jù)處理過程中,使數(shù)據(jù)處于鎖定狀態(tài)羊娃。悲觀鎖的實現(xiàn)往往依靠數(shù)據(jù)庫提供的鎖機制唐全,即在數(shù)據(jù)庫中,在對數(shù)據(jù)記錄擦做前給記錄加排它鎖蕊玷。如果獲取鎖失敗邮利,則說明數(shù)據(jù)正在被其他線程修改,當(dāng)前線程則等待或者拋出異常垃帅。如果獲取鎖成功延届,則對記錄進行操作,然后提交事務(wù)后釋放排它鎖
- 樂觀鎖認為數(shù)據(jù)在一般情況下不會造成沖突贸诚,所以在訪問記錄前不會加排它鎖方庭,而是在進行數(shù)據(jù)提交更新時,才會正式對數(shù)據(jù)沖突與否進行檢測酱固。
- 樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機制械念,一般在表中添加version字段或者使用業(yè)務(wù)狀態(tài)來實現(xiàn)。樂觀鎖知道提交時才鎖定运悲,所以不會產(chǎn)生任何死鎖
- 樂觀鎖悲觀鎖分別有哪些龄减?
-
公平鎖與非公平鎖
根據(jù)線程獲取鎖的搶占機制劃分
公平鎖表示線程按照先來先到的方式獲取鎖,而非公平鎖則是隨機分配鎖
-
ReentrantLock提供了公平和非公平鎖的實現(xiàn)
公平鎖:ReentrantLock pairLock = new ReentrantLock(true)
非公平鎖:ReentrantLock noPairLock = new ReentrantLock(false)扇苞。如果構(gòu)造函數(shù)不傳遞參數(shù)欺殿,則默認是非公平鎖。 在沒有公平需求的前提下盡量使用非公平鎖鳖敷,因為公平鎖會帶來性能開銷
-
獨占鎖與共享鎖
- 根據(jù)鎖只能被單個線程持有還是能被多個線程共同持有劃分
- 獨占鎖保證任何時候都只有一個線程能得到鎖脖苏,ReentrantLock就是以獨占方式實現(xiàn)的。共享鎖則可以同時由多個線程持有定踱,例如ReadWriteLock讀寫鎖棍潘,它允許一個資源可以被多個線程同時進行讀操作
- 獨占鎖是一種悲觀鎖,由于每次訪問資源都先加上互斥鎖,這限制了并發(fā)性亦歉,因為讀操作并不會影響數(shù)據(jù)的一致性恤浪,而獨占鎖只允許在同一時間由一個線程讀取數(shù)據(jù),其他線程必須等待當(dāng)前線程釋放鎖才能進行讀取
- 共享鎖則是一種樂觀鎖肴楷,它放寬了加鎖條件水由,允許多個線程同時進行讀操作
-
什么是可重入鎖
-
當(dāng)一個線程要獲取一個被其他線程持有的獨占鎖時,該線程會被阻塞赛蔫,那么當(dāng)一個線程再次獲取它自己已經(jīng)獲取的鎖時砂客,如果不被阻塞,那么該鎖就是可重入的呵恢,也就是只要該線程獲取了該鎖鞠值,可以有限次的進入被該鎖鎖住的代碼
- 子主題 1
synchronized內(nèi)部鎖是可重入鎖。
原理:在鎖內(nèi)部維護一個線程標(biāo)示渗钉,用來標(biāo)示該鎖目前被哪個線程占用彤恶,然后關(guān)聯(lián)一個計數(shù)器。一開始計數(shù)器值為0鳄橘,說明該鎖沒有被任何線程占用声离。當(dāng)一個線程獲取了該鎖時,計數(shù)器的值會變成1挥唠,這時其他線程再來獲取該鎖時會發(fā)現(xiàn)鎖的所有者不是自己而被阻塞掛起抵恋。但是當(dāng)獲取了該鎖的線程再次獲取鎖時發(fā)現(xiàn)擁有者是自己焕议,就會把計數(shù)值+1宝磨,當(dāng)釋放鎖后計數(shù)器值-1.當(dāng)計數(shù)器值為0時,所里面的線程表示被重置為null盅安,這時候被阻塞的線程會被喚醒來競爭獲取該鎖
-
-
自旋鎖
- 當(dāng)一個線程在獲取鎖失敗后唤锉,會被切換到內(nèi)核狀態(tài)而被掛起。當(dāng)該線程獲取到鎖時又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程别瞭。而從用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的窿祥,在一定程度上會影響并發(fā)性能。
- 自選鎖是蝙寨,當(dāng)前線程在獲取鎖時晒衩,如果發(fā)現(xiàn)已經(jīng)被其他線程占有,他不馬上阻塞自己墙歪,在部放棄CPU使用權(quán)的情況下听系,多次嘗試獲取(默認10次虹菲,可以使用-XX:PreBlockSpinsh參數(shù)設(shè)置)靠胜,很有可能在后面幾次嘗試中其他線程已經(jīng)釋放了鎖。如果嘗試制定的次數(shù)后仍沒有獲取到鎖則當(dāng)前線程才會被阻塞掛起。自旋鎖是使用CPU時間換取線程阻塞與調(diào)度的開銷浪漠,但是很有可能這些CPU時間白白浪費了陕习。
數(shù)據(jù)庫中的悲觀鎖和樂觀鎖及其實現(xiàn)
-
總結(jié)
Java并發(fā)編程高級篇
Java并發(fā)包中ThreadLocalRandom類原理剖析
-
Random類及其局限性
- 多個線程使用同一個原子性種子變量,使用CAS址愿,從而導(dǎo)致對原子變量更新的競爭
- 自選重試導(dǎo)致的降低并發(fā)性能
-
ThreadLocalRandom
- 每個線程都維護一個種子變量该镣,每個線程生成隨機數(shù)時都根據(jù)自己老的種子計算新的種子,并使用新種子更新老的種子响谓,再根絕新種子計算隨機數(shù)拌牲,就不會存在競爭問題了,這大大提供并發(fā)性能
- 并發(fā)性能:ThreadLocal > CAS
- 并發(fā)性能更好
- 重寫了nextInt()及相關(guān)方法歌粥,在current()生成種子時塌忽,使用了System.currentTimeMills()和System.nanoTime()
- 計算新種子時是根據(jù)自己線程內(nèi)維護的種子變啊零進行更新,從而避免了競爭
-
源碼分析
-
類圖結(jié)構(gòu)
- 子主題 1
-
主要代碼的實現(xiàn)邏輯
- Unsafe機制
- ThreadLocalRandom current()方法
- int nextInt(int bound)
-
總結(jié)
-
問題
使用方式有問題失驶,如果只實例化一個實例土居,然后在多個線程中訪問,不就不需要使用處理并發(fā)問題了嬉探?
為什么要AtomicLong修飾擦耀,不是局部變量么?又不是共享變量涩堤?
-
兩個seed有啥不同眷蜓?
- 賦值對象的化,是相同的引用
Atomic類型變量的原子性在這里有啥用胎围?
ThreadLocalRandom對相同實例吁系,不會造成生成相同的偽隨機數(shù)嗎?
Java并發(fā)包中原子操作類原理剖析
JUC提供了一系列的原子性操作類白魂,這些類都是使用非阻塞算法CAS實現(xiàn)的汽纤,相比使用鎖實現(xiàn)原子性操作這在性能上有很大提高
原子操作對可見性、有序性福荸、原子性的保證?
-
原子變量操作類
- JUC并發(fā)包中包含有AtomicInteger蕴坪、AtomicLong和AtomicBoolean等原子性操作類,它們的原理類似
- 內(nèi)部使用Unsafe來實現(xiàn)
雖然Atomic系原子操作類使用CAS非阻塞算法敬锐,但是在高并發(fā)情況下AtomicLong還會存在性能問題背传。因此提供了高并發(fā)下性能更好的LongAdder類
-
JDK 8新增的原子操作類LongAdder
-
LongAdder簡單介紹
- 為了克服AtomicLong由于多線程同時去競爭一個變量的更新而產(chǎn)生的,那么把一個變量分解為多個變量台夺,讓同樣多的線程去競爭多個資源径玖,就解決了性能問題
- 使用AtomicLong時,多個線程同時競爭同一個原子變量
- 使用LongAdder時谒养,則是在內(nèi)部維護多個Cell變量挺狰,每個Cell里面有一個初始值為0的long型變量明郭,這樣,在同等并發(fā)量的情況下丰泊,爭奪單個變量更新操作的線程量會減少薯定,這變相減少了爭奪共享資源的并發(fā)量。另外瞳购,多個線程在爭奪同一個Cell原子變量時如果失敗了话侄,它并不是在當(dāng)前Cell變量上一直自旋CAS重試,而是嘗試在其他Cell的變量上進行CAS嘗試学赛,這個改變增加了當(dāng)前線程重試CAS成功的可能性浪蹂。最后垮耳,在獲取LongAdder當(dāng)前值時,是把所有Cell變量的value值累加后再加上base返回的
-
LongAdder代碼分析
-
問題
- LongAdder的結(jié)構(gòu)是怎么樣的?
- 當(dāng)前線程應(yīng)該訪問Cell數(shù)組里面的哪一個Cell元素闷堡?
- 如何初始化Cell數(shù)組喉前?
- Cell數(shù)組如何擴容佳吞?
- 線程訪問分配的Cell元素有沖突后如何處理贮勃?
- 如何保證線程操作被分配的Cell元素的原子性
-
類圖
-
小結(jié)
-
-
LongAccumulator類原理探究
LongAdder類是LongAccumulator的一個特里,LongAccumulator比LongAdder的功能更強大滴劲。
-
構(gòu)造函數(shù)
- 子主題 1
傳的參數(shù)是個函數(shù)是接口類型攻晒,可以自定義計算類型,LongAdder是制定了累加運算班挖,
關(guān)鍵是函數(shù)式接口類型參數(shù)
總結(jié)
Java并發(fā)包中List源碼剖析
-
介紹
- 并發(fā)包中的List只有CopyOnWriteArrayList
- CopyOnWriteArrayList是一個線程安全底ArrayList鲁捏,對其進行的修改操作都是再底層的一個復(fù)制的數(shù)組(快照)上進行的,也就是使用了寫時復(fù)制策略
-
類圖
- 子主題 1
- ReentrantLock獨占鎖對象用來保證同時只有一個線程對aray進行修改
-
問題
-
如果讓我們自己做一個寫時復(fù)制的線程安全的list我們會怎么做萧芙,有哪些點需要考慮给梅?
-
何時初始化list,初始化的list元素個數(shù)為多少末购,list是有限大小嗎破喻?
- 無界list
如何保證線程安全,比如多個線程進行讀寫時如何保證是線程安全的盟榴?
如何保證使用迭代器遍歷list時的數(shù)據(jù)一致性?
如何保證性能婴噩?
-
-
-
主要方法源碼剖析
初始化
-
添加元素
- 數(shù)組賦值時不是傳的引用么擎场?
獲取指定位置元素
修改指定元素
-
刪除元素
- 出現(xiàn)弱一致性問題
-
弱一致性的迭代器
- 弱一致性是指返回迭代器后,其他線程對list的增刪改對迭代器是不可見的几莽,因為迭代和增刪改操作的是兩個不同的數(shù)組
- COWIterator對象的snapshot變量保存了當(dāng)前l(fā)ist的內(nèi)容迅办,cursor是遍歷list時數(shù)據(jù)的下表
- 老數(shù)組被snapshot引用
總結(jié)
-
相似實現(xiàn)
- CopyOnWriteArraySet
Java并發(fā)包中鎖原理剖析
-
LockSupport工具類
-
介紹
- JDK中rt.jar報里面的LockSupport是要個工具類,它的主要作用是掛起和喚醒線程章蚣,該工具類是創(chuàng)建鎖和其他同步類的基礎(chǔ)
- LockSupport類與每個使用它的線程都會關(guān)聯(lián)一個許可證站欺,在默認情況下調(diào)用LockSupport類的方法的線程是不持有許可證的。LockSupport是使用Unsafe類實現(xiàn)的
-
基本方法
-
void park()
- 如果調(diào)用park()的線程已經(jīng)拿到了與LockSupport關(guān)聯(lián)的許可證,則調(diào)用LockSupport.park()時會馬上返回矾策,否則調(diào)用線程會被阻塞掛起
- 返回方式是使用unpark(Thread thread)或者調(diào)用阻塞線程的interrupt()
-
void unpark(Thread thread)
- 當(dāng)一個線程調(diào)用unpark時磷账,如果參數(shù)thread線程沒有持有thread與LockSupport類關(guān)聯(lián)許可證,則讓thread線程持有贾虽。如果thread之前因調(diào)用park()而被掛起逃糟,則調(diào)用unpark()后,該線程會被喚醒蓬豁。如果thread之前沒有調(diào)用park绰咽,則調(diào)用unpark方法后,再調(diào)用park方法地粪,其會立刻返回
- 簡而言之取募,就是給參數(shù)線程一個LockSupport類關(guān)聯(lián)的許可證
- 如果調(diào)用兩次unpark()會出現(xiàn)什么情況呢?
-
void parkNanos(long nanos)
- 和park方法類似蟆技。區(qū)別是矛辕,如果沒有拿到許可證,則調(diào)用線程會被掛起nanos時間后修改為自動返回
-
void park(Object blocker)
- 當(dāng)線程沒有持有許可證的情況下付魔,調(diào)用park方法而被阻塞時聊品,這個blocker對象會被記錄到該線程內(nèi)部
- 使用診斷工具可以觀察線程被阻塞的原因,診斷工具是通過調(diào)用getBlocker(Thread)方法來獲取blocker對象的几苍,所以JDK推薦我們使用帶有blocker參數(shù)的park方法翻屈,并且blocker被這是為this,這樣擋在打印線程堆棧排查問題時就能知道是哪個類被阻塞了
- 使用帶有blocker參數(shù)的park方法妻坝,線程堆椛炜簦可以提供更多有關(guān)阻塞對象的信息
-
void parkNanos(Object blocker, long nanos)
- 相比park(Object blocker)多了個超時時間
-
void parkUntil(Object blocker, long deadline)
- 制定毫秒的時間點
-
獲取當(dāng)前線程Thread.currentThread()
-
-
抽象同步隊列AQS概述
AbstractQueuedSynchronizer抽象同步隊列簡稱AQS,它是實現(xiàn)同步器的基礎(chǔ)組件刽宪,并發(fā)包中鎖的底層就是使用AQS實現(xiàn)的厘贼。雖然一般不會直接使用AQS,但是直到其原理對于架構(gòu)設(shè)計有幫助
-
鎖的底層支持
-
類圖
- 子主題 1
-
-
條件變量的支持
-
說明
-
notify和wait是配合synchronized內(nèi)置鎖實現(xiàn)線程間同步的基礎(chǔ)設(shè)施
- 在調(diào)用共享變量的notify和wait方法前必須首先獲取該共享變量的內(nèi)置鎖圣拄。在調(diào)用條件變量的signal和await方法前必須限獲取條件變量對應(yīng)的鎖
條件變量signal和await方法是配合使用AQS實現(xiàn)的鎖來實現(xiàn)線程間同步的基礎(chǔ)設(shè)施
-
-
基于AQS實現(xiàn)自定義同步器
-
獨占鎖ReentrantLock的原理
- 類圖結(jié)構(gòu)
- 獲取鎖
- 釋放鎖
- 案例介紹
- 小結(jié)
-
讀寫鎖ReentrantReadWriteLock的原理
- 類圖結(jié)構(gòu)
- 寫鎖的獲取與釋放
- 讀鎖的獲取與釋放
- 案例介紹
- 小結(jié)
-
JDK 8中新增的StampedLock鎖探究
- 概述
- 案例介紹
- 小結(jié)
-
Java并發(fā)包中并發(fā)隊列原理剖析
-
ConcurrentLinkedQueue原理探究
- 類圖結(jié)構(gòu)
- ConcurrentLinkedQueue原理介紹
- 小結(jié)
-
LinkedBlockingQueue原理探究
- 類圖結(jié)構(gòu)
- LinkedBlockingQueue原理介紹
- 小結(jié)
-
ArrayBlockingQueue原理探究
- 類圖結(jié)構(gòu)
- ArrayBlockingQueue原理介紹
- 小結(jié)
-
PriorityBlockingQueue
- 介紹
- 類圖結(jié)構(gòu)
- 原理介紹
- 案例介紹
- 小結(jié)
-
DelayQueue原理探究
- 類圖結(jié)構(gòu)
- 主要函數(shù)原理講解
- 案例介紹
- 小結(jié)
-
-
Java并發(fā)包中線程池ThreadPoolExecutor原理探究
介紹
類圖介紹
-
源碼分析
- public void execute(Runnable command)
- 工作線程Worker的執(zhí)行
- shutdown操作
- shutdownNow操作
- awaitTermination操作
總結(jié)
-
Java并發(fā)包中ScheduledThreadPoolExecutor原理探究
介紹
類圖介紹
-
原理剖析
- schedule(Runnable command, long delay, TimeUnit unit)
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
總結(jié)
-
Java并發(fā)包中線程同步原理剖析
-
CountDownLatch原理剖析
- 案例介紹
- 實現(xiàn)原理探究
- 小結(jié)
-
回環(huán)屏障CyclicBarrier原理探究
- 案例介紹
- 實現(xiàn)原理探究
- 小結(jié)
-
信號量Seaphore原理探究
- 案例介紹
- 實現(xiàn)原理探究
- 小結(jié)
總結(jié)
-
Java并發(fā)編程實踐篇
并發(fā)編程實踐
-
ArrayBlockingQueue的使用
- 異步日志打印模型概述
- 異步日志與具體實現(xiàn)
- 小結(jié)
-
Tomcat的NioEndPoint中ConcurrentLinkedQueue的使用
- 生產(chǎn)者線程——Acceptor線程
- 消費者——Poller線程
- 小結(jié)
并發(fā)組件ConcurrentHashMap使用注意事項
-
SimpleDateFormat是線程不安全的
- 問題復(fù)現(xiàn)
- 問題分析
- 小結(jié)
-
使用Timer時需要注意的事情
- 問題的產(chǎn)生
- Timer實現(xiàn)原理分析
- 小結(jié)
-
對需要復(fù)用但是會被下游修改的參數(shù)進行深復(fù)制
- 問題的產(chǎn)生
- 問題分析
- 小結(jié)
-
創(chuàng)建線程和線程池時要指定與業(yè)務(wù)相關(guān)的名稱
- 創(chuàng)建線程需要有線程名
- 創(chuàng)建線程池時也需要指定線程池的名稱
- 小結(jié)
-
使用線程池的情況下當(dāng)程序結(jié)束時記得調(diào)用shutdown關(guān)閉線程池
- 問題復(fù)現(xiàn)
- 問題分析
- 小結(jié)
-
線程池使用FutureTask時需要注意的事情
- 問題復(fù)現(xiàn)
- 問題分析
- 小結(jié)
-
使用ThreadLocal不當(dāng)可能導(dǎo)致內(nèi)存泄漏
- 為何會出現(xiàn)內(nèi)存泄漏
- 在線程中使用ThreadLocal導(dǎo)致的內(nèi)存泄漏
- 在Tomcat的Servlet中使用ThreadLocal導(dǎo)致內(nèi)存泄漏
- 小結(jié)
總結(jié)
XMind: ZEN - Trial Version