Java并發(fā)編程(二):線程基礎(chǔ)鞍帝、線程之間的共享與合作


一、基礎(chǔ)概念

1厢漩、進(jìn)程和線程

進(jìn)程是程序運(yùn)行資源分配的最小單位

進(jìn)程是操作系統(tǒng)進(jìn)行資源分配的最小單位,其中資源包括:CPU膜眠、內(nèi)存空間、磁盤 IO 等,同一進(jìn)程中的多條線程共享該進(jìn)程中的全部系統(tǒng)資源,而進(jìn)程和進(jìn)程之間是相互獨(dú)立的溜嗜。進(jìn)程是具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動,進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位宵膨。顯然,程序是死的、靜態(tài)的,進(jìn)程是活的炸宵、動態(tài)的辟躏。進(jìn)程可以分為系統(tǒng)進(jìn)程和用戶進(jìn)程。凡是用于完成操作系統(tǒng)的各種功能的進(jìn)程就是系統(tǒng)進(jìn)程,它們就是處于運(yùn)行狀態(tài)下的操作系統(tǒng)本身,用戶進(jìn)程就是所有由你啟動的進(jìn)程土全。

線程是 CPU 調(diào)度的最小單位,必須依賴于進(jìn)程而存在

線程是進(jìn)程的一個(gè)實(shí)體,是 CPU 調(diào)度和分派的基本單位,它是比進(jìn)程更小的捎琐、能獨(dú)立運(yùn)行的基本單位。線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源裹匙。

2瑞凑、CPU 核心數(shù)和線程數(shù)的關(guān)系

多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱 CMP),CMP 是由美國斯坦福大學(xué)提出的,其思想是將大規(guī)模并行處理器中的 SMP(對稱多處理器)集成到同一芯片內(nèi),各個(gè)處理器并行執(zhí)行不同的進(jìn)程。這種依靠多個(gè) CPU 同時(shí)并行地運(yùn)行程序是實(shí)現(xiàn)超高速計(jì)算的一個(gè)重要方向,稱為并行處理概页。

多線程: Simultaneous Multithreading.簡稱 SMT.讓同一個(gè)處理器上的多個(gè)線程同步執(zhí)行并共享處理器的執(zhí)行資源籽御。

核心數(shù)、線程數(shù):目前主流 CPU 都是多核的。增加核心數(shù)目就是為了增加線程數(shù),因?yàn)椴僮飨到y(tǒng)是通過線程來執(zhí)行任務(wù)的,一般情況下它們是 1:1 對應(yīng)關(guān)系,也就是說四核 CPU 一般擁有四個(gè)線程技掏。但 Intel 引入超線程技術(shù)后,使核心數(shù)與線程數(shù)形成 1:2 的關(guān)系

3铃将、CPU 時(shí)間片輪轉(zhuǎn)機(jī)制

我們平時(shí)在開發(fā)的時(shí)候,感覺并沒有受 cpu 核心數(shù)的限制哑梳,想啟動線程就啟動線程劲阎,哪怕是在單核 CPU 上,為什么鸠真?這是因?yàn)椴僮飨到y(tǒng)提供了一種 CPU 時(shí)間片輪轉(zhuǎn)機(jī)制悯仙。時(shí)間片輪轉(zhuǎn)調(diào)度是一種最古老、最簡單吠卷、最公平且使用最廣的算法,又稱 RR調(diào)度雁比。每個(gè)進(jìn)程被分配一個(gè)時(shí)間段,稱作它的時(shí)間片,即該進(jìn)程允許運(yùn)行的時(shí)間。百度百科對 CPU 時(shí)間片輪轉(zhuǎn)機(jī)制原理解釋如下:如果在時(shí)間片結(jié)束時(shí)進(jìn)程還在運(yùn)行,則 CPU 將被剝奪并分配給另一個(gè)進(jìn)程撤嫩。如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)束,則 CPU 當(dāng)即進(jìn)行切換偎捎。調(diào)度程序所要做的就是維護(hù)一張就緒進(jìn)程列表,當(dāng)進(jìn)程用完它的時(shí)間片后,它被移到隊(duì)列的末尾。時(shí)間片輪轉(zhuǎn)調(diào)度中唯一有趣的一點(diǎn)是時(shí)間片的長度序攘。從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程是需要一定時(shí)間的,包括保存和裝入寄存器值及內(nèi)存映像,更新各種表格和隊(duì)列等茴她。假如進(jìn)程切換( processwitch),有時(shí)稱為上下文切換( context switch,),需要 5ms, 再假設(shè)時(shí)間片設(shè)為 20ms,則在做完 20ms 有用的工作之后,CPU 將花費(fèi) 5ms 來進(jìn)行進(jìn)程切換程奠。CPU 時(shí)間的 20%被浪費(fèi)在了管理開銷上了丈牢。為了提高 CPU 效率,我們可以將時(shí)間片設(shè)為 5000ms。這時(shí)浪費(fèi)的時(shí)間只有0.1%瞄沙。但考慮到在一個(gè)分時(shí)系統(tǒng)中,如果有 10 個(gè)交互用戶幾乎同時(shí)按下回車鍵, 將發(fā)生什么情況?假設(shè)所有其他進(jìn)程都用足它們的時(shí)間片的話,最后一個(gè)不幸的進(jìn)程不得不等待 5s 才獲得運(yùn)行機(jī)會己沛。多數(shù)用戶無法忍受一條簡短命令要 5 s才能做出響應(yīng),同樣的問題在一臺支持多道程序的個(gè)人計(jì)算機(jī)上也會發(fā)生。結(jié)論可以歸結(jié)如下:時(shí)間片設(shè)得太短會導(dǎo)致過多的進(jìn)程切換,降低了 CPU 效率:而設(shè)得太長又可能引起對短的交互請求的響應(yīng)變差距境。將時(shí)間片設(shè)為 100ms 通常是一個(gè)比較合理的折衷申尼。

在 CPU 死機(jī)的情況下,其實(shí)大家不難發(fā)現(xiàn)當(dāng)運(yùn)行一個(gè)程序的時(shí)候把 CPU 給弄到了 100%再不重啟電腦的情況下,其實(shí)我們還是有機(jī)會把它 KⅢ掉的,我想也正是因?yàn)檫@種機(jī)制的緣故。?

4垫桂、澄清并行和并發(fā)

我們舉個(gè)例子,如果有條高速公路 A 上面并排有 8 條車道,那么最大的并行車輛就是 8 輛此條高速公路 A 同時(shí)并排行走的車輛小于等于 8 輛的時(shí)候,車輛就可以并行運(yùn)行愧怜。CPU 也是這個(gè)原理,一個(gè) CPU 相當(dāng)于一個(gè)高速公路 A,核心數(shù)或者線程數(shù)就相當(dāng)于并排可以通行的車道;而多個(gè) CPU 就相當(dāng)于并排有多條高速公路,而每個(gè)高速公路并排有多個(gè)車道按脚。當(dāng)談?wù)摬l(fā)的時(shí)候一定要加個(gè)單位時(shí)間,也就是說單位時(shí)間內(nèi)并發(fā)量是多少?離開了單位時(shí)間其實(shí)是沒有意義的。俗話說,一心不能二用,這對計(jì)算機(jī)也一樣,原則上一個(gè) CPU 只能分配給一個(gè)進(jìn)程,以便運(yùn)行這個(gè)進(jìn)程历恐。我們通常使用的計(jì)算機(jī)中只有一個(gè) CPU,也就是說只有一顆心,要讓它一心多用同時(shí)運(yùn)行多個(gè)進(jìn)程,就必須使用并發(fā)技術(shù)缓窜。實(shí)現(xiàn)并發(fā)技術(shù)

相當(dāng)復(fù)雜,最容易理解的是“時(shí)間片輪轉(zhuǎn)進(jìn)程調(diào)度算法”嫂沉。綜合來說:并發(fā):指應(yīng)用能夠交替執(zhí)行不同的任務(wù),比如單 CPU 核心下執(zhí)行多線程并非是同時(shí)執(zhí)行多個(gè)任務(wù),如果你開兩個(gè)線程執(zhí)行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個(gè)任務(wù),已達(dá)到"同時(shí)執(zhí)行效果",其實(shí)并不是的,只是計(jì)算機(jī)的速度太快,我們無法察覺到而已. 并行:指應(yīng)用能夠同時(shí)執(zhí)行不同的任務(wù),例:吃飯的時(shí)候可以邊吃飯邊打電話, 這兩件事情可以同時(shí)執(zhí)行兩者區(qū)別:一個(gè)是交替執(zhí)行,一個(gè)是同時(shí)執(zhí)行.


5殴蓬、高并發(fā)編程的意義柏蘑、好處

由于多核多線程的 CPU 的誕生,多線程、高并發(fā)的編程越來越受重視和關(guān)注空镜。多線程可以給程序帶來如下好處浩淘。?

(1)充分利用 CPU 的資源

從上面的 CPU 的介紹,可以看的出來,現(xiàn)在市面上沒有 CPU 的內(nèi)核不使用多線程并發(fā)機(jī)制的,特別是服務(wù)器還不止一個(gè) CPU,如果還是使用單線程的技術(shù)做思路, 明顯就 out 了矾利。因?yàn)槌绦虻幕菊{(diào)度單元是線程,并且一個(gè)線程也只能在一個(gè) CPU的一個(gè)核的一個(gè)線程跑,如果你是個(gè) i3 的 CPU 的話,最差也是雙核心 4 線程的運(yùn)算能力:如果是一個(gè)線程的程序的話,那是要浪費(fèi) 3/4 的 CPU 性能:如果設(shè)計(jì)一個(gè)多線程的程序的話,那它就可以同時(shí)在多個(gè) CPU 的多個(gè)核的多個(gè)線程上跑,可以充分地利用 CPU,減少 CPU 的空閑時(shí)間,發(fā)揮它的運(yùn)算能力,提高并發(fā)量。就像我們平時(shí)坐地鐵一樣,很多人坐長線地鐵的時(shí)候都在認(rèn)真看書,而不是為了坐地鐵而坐地鐵,到家了再去看書,這樣你的時(shí)間就相當(dāng)于有了兩倍馋袜。這就是為什么有些人時(shí)間很充裕,而有些人老是說沒時(shí)間的一個(gè)原因,工作也是這樣,有的時(shí)候可以并發(fā)地去做幾件事情,充分利用我們的時(shí)間,CPU 也是一樣,也要充分利用。

(2)加快響應(yīng)用戶的時(shí)間

比如我們經(jīng)常用的迅雷下載,都喜歡多開幾個(gè)線程去下載,誰都不愿意用一個(gè)線程去下載,為什么呢?答案很簡單,就是多個(gè)線程下載快啊舶斧。我們在做程序開發(fā)的時(shí)候更應(yīng)該如此,特別是我們做互聯(lián)網(wǎng)項(xiàng)目,網(wǎng)頁的響應(yīng)時(shí)間若提升 1s,如果流量大的話,就能增加不少轉(zhuǎn)換量欣鳖。做過高性能 web 前端調(diào)優(yōu)的都知道,要將靜態(tài)資源地址用兩三個(gè)子域名去加載,為什么?因?yàn)槊慷嘁粋€(gè)子域名,瀏覽器在加載你的頁面的時(shí)候就會多開幾個(gè)線程去加載你的頁面資源,提升網(wǎng)站的響應(yīng)速度。多線程,高并發(fā)真的是無處不在茴厉。

(3)可以使你的代碼模塊化,異步化,簡單化

例如我們實(shí)現(xiàn)電商系統(tǒng)泽台,下訂單和給用戶發(fā)送短信、郵件就可以進(jìn)行拆分矾缓,將給用戶發(fā)送短信怀酷、郵件這兩個(gè)步驟獨(dú)立為單獨(dú)的模塊,并交給其他線程去執(zhí)行嗜闻。這樣既增加了異步的操作蜕依,提升了系統(tǒng)性能,又使程序模塊化,清晰化和簡單化琉雳。多線程應(yīng)用開發(fā)的好處還有很多,大家在日后的代碼編寫過程中可以慢慢體會它的魅力样眠。?

6、多線程程序需要注意事項(xiàng)

(1)線程之間的安全性

從前面的章節(jié)中我們都知道,在同一個(gè)進(jìn)程里面的多線程是資源共享的,也就是都可以訪問同一個(gè)內(nèi)存地址當(dāng)中的一個(gè)變量翠肘。例如:若每個(gè)線程中對全局變量檐束、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個(gè)全局變量是線程安全的:若有多個(gè)線程同時(shí)執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。

(2)線程之間的死鎖

為了解決線程之間的安全性引入了 Java 的鎖機(jī)制,而一不小心就會產(chǎn)生 Java線程死鎖的多線程問題,因?yàn)椴煌木€程都在等待那些根本不可能被釋放的鎖,從而導(dǎo)致所有的工作都無法完成束倍。假設(shè)有兩個(gè)線程,分別代表兩個(gè)饑餓的人,他們必須共享刀叉并輪流吃飯被丧。他們都需要獲得兩個(gè)鎖:共享刀和共享叉的鎖。假如線程 A 獲得了刀,而線程 B 獲得了叉绪妹。線程 A 就會進(jìn)入阻塞狀態(tài)來等待獲得叉,而線程 B 則阻塞來等待線程 A 所擁有的刀甥桂。這只是人為設(shè)計(jì)的例子,但盡管在運(yùn)行時(shí)很難探測到,這類情況卻時(shí)常發(fā)生

(3)線程太多了會將服務(wù)器資源耗盡形成死機(jī)當(dāng)機(jī)

線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存以及 CPU的“過渡切換”,造成系統(tǒng)的死機(jī),那么我們該如何解決這類問題呢?

某些系統(tǒng)資源是有限的,如文件描述符。多線程程序可能耗盡資源,因?yàn)槊總€(gè)線程都可能希望有一個(gè)這樣的資源邮旷。如果線程數(shù)相當(dāng)大,或者某個(gè)資源的侯選線程數(shù)遠(yuǎn)遠(yuǎn)超過了可用的資源數(shù)則最好使用資源池格嘁。一個(gè)最好的示例是數(shù)據(jù)庫連接池。只要線程需要使用一個(gè)數(shù)據(jù)庫連接,它就從池中取出一個(gè),使用以后再將它返回池中廊移。資源池也稱為資源庫糕簿。多線程應(yīng)用開發(fā)的注意事項(xiàng)很多,希望大家在日后的工作中可以慢慢體會它的危險(xiǎn)所在。?

二狡孔、認(rèn)識 Java 里的線程



1懂诗、Java 程序天生就是多線程的

一個(gè) Java 程序從 main()方法開始執(zhí)行,然后按照既定的代碼邏輯執(zhí)行苗膝,看似沒有其他線程參與殃恒,但實(shí)際上 Java 程序天生就是多線程程序,因?yàn)閳?zhí)行 main()方法的是一個(gè)名稱為 main 的線程。

[6] Monitor Ctrl-Break //監(jiān)控 Ctrl-Break 中斷信號的

[5] Attach Listener //內(nèi)存 dump离唐,線程 dump病附,類信息統(tǒng)計(jì),獲取系統(tǒng)屬性等

[4] Signal Dispatcher // 分發(fā)處理發(fā)送給 JVM 信號的線程

[3] Finalizer // 調(diào)用對象 finalize 方法的線程

[2] Reference Handler//清除 Reference 的線程

[1] main //main 線程亥鬓,用戶程序入口

2完沪、線程的啟動與中止


(1)啟動

啟動線程的方式有

1、X extends Thread;嵌戈,然后 X.start

2覆积、X implements Runnable;然后交給 Thread 運(yùn)行

Thread 和 Runnable 的區(qū)別

Thread 才是 Java 里對線程的唯一抽象熟呛,Runnable 只是對任務(wù)(業(yè)務(wù)邏輯)的抽象宽档。Thread 可以接受任意一個(gè) Runnable 的實(shí)例并執(zhí)行。

(2)中止

線程自然終止

要么是 run 執(zhí)行完成了庵朝,要么是拋出了一個(gè)未處理的異常導(dǎo)致線程提前結(jié)束吗冤。

stop

暫停、恢復(fù)和停止操作對應(yīng)在線程 Thread 的 API 就是 suspend()九府、resume()和 stop()欣孤。但是這些 API 是過期的,也就是不建議使用的昔逗。不建議使用的原因主要有:以 suspend()方法為例降传,在調(diào)用后,線程不會釋放已經(jīng)占有的資源(比如鎖)勾怒,而是占有著資源進(jìn)入睡眠狀態(tài)婆排,這樣容易引發(fā)死鎖問題。同樣笔链,stop()方法在終結(jié)一個(gè)線程時(shí)不會保證線程的資源正常釋放段只,通常是沒有給予線程完成資源釋放工作的機(jī)會,因此會導(dǎo)致程序可能工作在不確定狀態(tài)下鉴扫。正因?yàn)?suspend()赞枕、resume()和 stop()方法帶來的副作用,這些方法才被標(biāo)注為不建議使用的過期方法坪创。

中斷

安全的中止則是其他線程通過調(diào)用某個(gè)線程 A 的 interrupt()方法對其進(jìn)行中斷操作, 中斷好比其他線程對該線程打了個(gè)招呼炕婶,“A,你要中斷了”莱预,不代表線程 A 會立即停止自己的工作柠掂,同樣的 A 線程完全可以不理會這種中斷請求。

因?yàn)?java 里的線程是協(xié)作式的依沮,不是搶占式的涯贞。線程通過檢查自身的中斷標(biāo)志位是否被置為 true 來進(jìn)行響應(yīng)枪狂,線程通過方法 isInterrupted()來進(jìn)行判斷是否被中斷,也可以調(diào)用靜態(tài)方法Thread.interrupted()來進(jìn)行判斷當(dāng)前線程是否被中斷宋渔,不過 Thread.interrupted()會同時(shí)將中斷標(biāo)識位改為 false州疾。如果一個(gè)線程處于了阻塞狀態(tài)(如線程調(diào)用了 thread.sleep、thread.join皇拣、thread.wait 等)严蓖,則在線程在檢查中斷標(biāo)示時(shí)如果發(fā)現(xiàn)中斷標(biāo)示為 true,則會在這些阻塞方法調(diào)用處拋出 InterruptedException 異常审磁,并且在拋出異常后會立即將線程的中斷標(biāo)示位清除,即重新設(shè)置為 false岂座。不建議自定義一個(gè)取消標(biāo)志位來中止線程的運(yùn)行态蒂。因?yàn)?run 方法里有阻塞調(diào)用時(shí)會無法很快檢測到取消標(biāo)志,線程必須從阻塞調(diào)用返回后费什,才會檢查這個(gè)取消標(biāo)志钾恢。這種情況下,使用中斷會更好鸳址,因?yàn)椋?/p>

一瘩蚪、一般的阻塞方法,如 sleep 等本身就支持中斷的檢查稿黍,

二疹瘦、檢查中斷位的狀態(tài)和檢查取消標(biāo)志位沒什么區(qū)別,用中斷位的狀態(tài)還可以避免聲明取消標(biāo)志位巡球,減少資源的消耗言沐。

注意:處于死鎖狀態(tài)的線程無法被中斷

3、深入理解 run()和 start()

Thread類是Java里對線程概念的抽象酣栈,可以這樣理解:我們通過new Thread()其實(shí)只是 new 出一個(gè) Thread 的實(shí)例险胰,還沒有和操作系統(tǒng)中真正的線程掛起鉤來。只有執(zhí)行了 start()方法后矿筝,才實(shí)現(xiàn)了真正意義上的啟動線程起便。start()方法讓一個(gè)線程進(jìn)入就緒隊(duì)列等待分配 cpu,分到 cpu 后才調(diào)用實(shí)現(xiàn)的 run()方法窖维,start()方法不能重復(fù)調(diào)用榆综,如果重復(fù)調(diào)用會拋出異常。而 run 方法是業(yè)務(wù)邏輯實(shí)現(xiàn)的地方铸史,本質(zhì)上和任意一個(gè)類的任意一個(gè)成員方法并沒有任何區(qū)別奖年,可以重復(fù)執(zhí)行,也可以被單獨(dú)調(diào)用沛贪。?

4陋守、其他的線程相關(guān)方法

(1)yield()方法

使當(dāng)前線程讓出 CPU 占有權(quán)震贵,但讓出的時(shí)間是不可設(shè)定的。也不會釋放鎖資源水评。注意:并不是每個(gè)線程都需要這個(gè)鎖的猩系,而且執(zhí)行 yield( )的線程不一定就會持有鎖,我們完全可以在釋放鎖后再調(diào)用 yield 方法中燥。所有執(zhí)行 yield()的線程有可能在進(jìn)入到就緒狀態(tài)后會被操作系統(tǒng)再次選中馬上又被執(zhí)行寇甸。

(2)wait()/notify()/notifyAll()

后面會單獨(dú)講述

5、join 方法

把指定的線程加入到當(dāng)前線程疗涉,可以將兩個(gè)交替執(zhí)行的線程合并為順序執(zhí)行拿霉。

比如在線程 B 中調(diào)用了線程 A 的 Join()方法,直到線程 A 執(zhí)行完畢后咱扣,才會繼續(xù)

執(zhí)行線程 B绽淘。(此處為常見面試考點(diǎn))

6、線程的優(yōu)先級

在 Java 線程中闹伪,通過一個(gè)整型成員變量 priority 來控制優(yōu)先級杀怠,優(yōu)先級的范圍從 1~10赔退,在線程構(gòu)建的時(shí)候可以通過 setPriority(int)方法來修改優(yōu)先級,默認(rèn)優(yōu)先級是 5卵渴,優(yōu)先級高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級低的線程。

設(shè)置線程優(yōu)先級時(shí)碘橘,針對頻繁阻塞(休眠或者 I/O 操作)的線程需要設(shè)置較高優(yōu)先級,而偏重計(jì)算(需要較多 CPU 時(shí)間或者偏運(yùn)算)的線程則設(shè)置較低的優(yōu)先級纺蛆,確保處理器不會被獨(dú)占温峭。在不同的 JVM 以及操作系統(tǒng)上,線程規(guī)劃會存在差異揖庄,有些操作系統(tǒng)甚至?xí)雎詫€程優(yōu)先級的設(shè)定。?

7检号、守護(hù)線程

Daemon(守護(hù))線程是一種支持型線程翘盖,因?yàn)樗饕挥米鞒绦蛑泻笈_調(diào)度以及支持性工作阁危。這意味著,當(dāng)一個(gè) Java 虛擬機(jī)中不存在非 Daemon 線程的時(shí)候趴乡,Java 虛擬機(jī)將會退出〉胄粒可以通過調(diào)用 Thread.setDaemon(true)將線程設(shè)置為 Daemon 線程玻淑。我們一般用不上岁忘,比如垃圾回收線程就是 Daemon 線程。

Daemon 線程被用作完成支持性工作麻汰,但是在 Java 虛擬機(jī)退出時(shí) Daemon 線程中的 finally 塊并不一定會執(zhí)行岔擂。在構(gòu)建 Daemon 線程時(shí)塑崖,不能依靠 finally 塊中的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯。

三、 線程間的共享和協(xié)作

1嗡髓、線程間的共享

(1)synchronized 內(nèi)置鎖

線程開始運(yùn)行,擁有自己的棧空間唆姐,就如同一個(gè)腳本一樣赵抢,按照既定的代碼一步一步地執(zhí)行,直到終止。但是摩渺,每個(gè)運(yùn)行中的線程,如果僅僅是孤立地運(yùn)行,那么沒有一點(diǎn)兒價(jià)值狂芋,或者說價(jià)值很少辆影,如果多個(gè)線程能夠相互配合完成工作,包括數(shù)據(jù)之間的共享次慢,協(xié)同處理事情旁涤。這將會帶來巨大的價(jià)值。Java 支持多個(gè)線程同時(shí)訪問一個(gè)對象或者對象的成員變量迫像,關(guān)鍵字synchronized 可以修飾方法或者以同步塊的形式來進(jìn)行使用劈愚,它主要確保多個(gè)線程在同一個(gè)時(shí)刻,只能有一個(gè)線程處于方法或者同步塊中闻妓,它保證了線程對變量訪問的可見性和排他性菌羽,又稱為內(nèi)置鎖機(jī)制。

對象鎖和類鎖

對象鎖是用于對象實(shí)例方法,或者一個(gè)對象實(shí)例上的奢讨,類鎖是用于類的靜態(tài)方法或者一個(gè)類的 class 對象上的描沟。我們知道佩伤,類的對象實(shí)例可以有很多個(gè)邀层,但是每個(gè)類只有一個(gè) class 對象估蹄,所以不同對象實(shí)例的對象鎖是互不干擾的系枪,但是每個(gè)類只有一個(gè)類鎖嘉栓。但是有一點(diǎn)必須注意的是尔崔,其實(shí)類鎖只是一個(gè)概念上的東西逞刷,并不是真實(shí)存在的,類鎖其實(shí)鎖的是每個(gè)類的對應(yīng)的 class 對象厅目。類鎖和對象鎖之間也是互不干擾的。

對象鎖和類鎖,以及鎖 static 變量之間的運(yùn)行情況郭宝,請參考包c(diǎn)n.enjoyedu.ch1.syn 下的代碼创倔。

?(2)錯(cuò)誤的加鎖和原因分析

參見代碼 cn.enjoyedu.ch1. syn.TestIntegerSyn

原因:雖然我們對 i 進(jìn)行了加鎖嗡害,但是但是當(dāng)我們反編譯這個(gè)類的 class 文件后,可以看到 i++實(shí)際是畦攘,本質(zhì)上是返回了一個(gè)新的 Integer 對象霸妹。也就是每個(gè)線程實(shí)際加鎖的是不同的 Integer 對象。

(3)volatile知押,最輕量的同步機(jī)制

volatile 保證了不同線程對這個(gè)變量進(jìn)行操作時(shí)的可見性叹螟,即一個(gè)線程修改了某個(gè)變量的值,這新值對其他線程來說是立即可見的台盯。參見代碼:

cn.enjoyedu.ch1.vola. VolatileCase

不加 volatile 時(shí)罢绽,子線程無法感知主線程修改了 ready 的值,從而不會退出循環(huán)静盅,而加了 volatile 后良价,子線程可以感知主線程修改了 ready 的值,迅速退出循環(huán)蒿叠。但是 volatile 不能保證數(shù)據(jù)在多個(gè)線程下同時(shí)寫時(shí)的線程安全明垢,參見代碼:

cn.enjoyedu.ch1.vola. NotSafe

volatile 最適用的場景:一個(gè)線程寫,多個(gè)線程讀栈虚。

2袖外、ThreadLocal 辨析

(1)與 Synchonized 的比較

ThreadLocal 和 Synchonized 都用于解決多線程并發(fā)訪問』晡瘢可是 ThreadLocal與 synchronized 有本質(zhì)的差別。synchronized 是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該僅僅能被一個(gè)線程訪問粘姜。而 ThreadLocal 為每個(gè)線程都提供了變量的副本鬓照,使得每個(gè)線程在某一時(shí)間訪問到的并非同一個(gè)對象,這樣就隔離了多個(gè)線程對數(shù)據(jù)的數(shù)據(jù)共享孤紧。Spring 的事務(wù)就借助了 ThreadLocal 類豺裆。Spring 會從數(shù)據(jù)庫連接池中獲得一個(gè)connection,然會把 connection 放進(jìn) ThreadLocal 中号显,也就和線程綁定了臭猜,事務(wù)需要提交或者回滾,只要從 ThreadLocal 中拿到 connection 進(jìn)行操作押蚤。為何 Spring的事務(wù)要借助 ThreadLocal 類蔑歌?以 JDBC 為例,正常的事務(wù)代碼可能如下:

dbc = new DataBaseConnection();//第 1 行

Connection con = dbc.getConnection();//第 2 行

con.setAutoCommit(false);// //第 3 行

con.executeUpdate(...);//第 4 行

con.executeUpdate(...);//第 5 行

con.executeUpdate(...);//第 6 行

con.commit();////第 7 行

上述代碼揽碘,可以分成三個(gè)部分:

事務(wù)準(zhǔn)備階段:第 1~3 行

業(yè)務(wù)處理階段:第 4~6 行

事務(wù)提交階段:第 7 行

可以很明顯的看到次屠,不管我們開啟事務(wù)還是執(zhí)行具體的 sql 都需要一個(gè)具體的數(shù)據(jù)庫連接。現(xiàn)在我們開發(fā)應(yīng)用一般都采用三層結(jié)構(gòu)雳刺,如果我們控制事務(wù)的代碼都放在DAO(DataAccessObject)對象中劫灶,在 DAO 對象的每個(gè)方法當(dāng)中去打開事務(wù)和關(guān)閉事務(wù),當(dāng) Service 對象在調(diào)用 DAO 時(shí)掖桦,如果只調(diào)用一個(gè) DAO本昏,那我們這樣實(shí)現(xiàn)則效果不錯(cuò),但往往我們的 Service 會調(diào)用一系列的 DAO 對數(shù)據(jù)庫進(jìn)行多次操作枪汪,那么凛俱,這個(gè)時(shí)候我們就無法控制事務(wù)的邊界了,因?yàn)閷?shí)際應(yīng)用當(dāng)中料饥,我們的 Service調(diào)用的 DAO 的個(gè)數(shù)是不確定的蒲犬,可根據(jù)需求而變化,而且還可能出現(xiàn) Service 調(diào)用 Service 的情況岸啡。如果不使用 ThreadLocal原叮,代碼大概就會是這個(gè)樣子:


但是需要注意一個(gè)問題,如何讓三個(gè) DAO 使用同一個(gè)數(shù)據(jù)源連接呢巡蘸?我們就必須為每個(gè) DAO 傳遞同一個(gè)數(shù)據(jù)庫連接奋隶,要么就是在 DAO 實(shí)例化的時(shí)候作為構(gòu)造方法的參數(shù)傳遞,要么在每個(gè) DAO 的實(shí)例方法中作為方法的參數(shù)傳遞悦荒。這兩種方式無疑對我們的 Spring 框架或者開發(fā)人員來說都不合適唯欣。為了讓這個(gè)數(shù)據(jù)庫連接可以跨階段傳遞,又不顯示的進(jìn)行參數(shù)傳遞搬味,就必須使用別的辦法境氢。Web 容器中蟀拷,每個(gè)完整的請求周期會由一個(gè)線程來處理。因此萍聊,如果我們能將一些參數(shù)綁定到線程的話问芬,就可以實(shí)現(xiàn)在軟件架構(gòu)中跨層次的參數(shù)共享(是隱式的共享)。而 JAVA 中恰好提供了綁定的方法--使用 ThreadLocal寿桨。結(jié)合使用 Spring 里的 IOC 和 AOP此衅,就可以很好的解決這一點(diǎn)。只要將一個(gè)數(shù)據(jù)庫連接放入 ThreadLocal 中亭螟,當(dāng)前線程執(zhí)行時(shí)只要有使用數(shù)據(jù)庫連接的地方就從 ThreadLocal 獲得就行了挡鞍。

(2)ThreadLocal 的使用

ThreadLocal 類接口很簡單,只有 4 個(gè)方法预烙,我們先來了解一下:

? void set(Object value)

設(shè)置當(dāng)前線程的線程局部變量的值墨微。

? public Object get()

該方法返回當(dāng)前線程所對應(yīng)的線程局部變量。

? public void remove()

將當(dāng)前線程局部變量的值刪除默伍,目的是為了減少內(nèi)存的占用欢嘿,該方法是 JDK5.0 新增的方法。需要指出的是也糊,當(dāng)線程結(jié)束后炼蹦,對應(yīng)該線程的局部變量將自動被垃圾回收,所以顯式調(diào)用該方法清除線程的局部變量并不是必須的操作狸剃,但它可以加快內(nèi)存回收的速度掐隐。

? protected Object initialValue()

返回該線程局部變量的初始值,該方法是一個(gè) protected 的方法钞馁,顯然是為了讓子類覆蓋而設(shè)計(jì)的虑省。這個(gè)方法是一個(gè)延遲調(diào)用方法,在線程第 1 次調(diào)用 get()或 set(Object)時(shí)才執(zhí)行僧凰,并且僅執(zhí)行 1 次探颈。ThreadLocal 中的缺省實(shí)現(xiàn)直接返回一個(gè) null。

public final static ThreadLocal<String> RESOURCE = newThreadLocal<String>();RESOURCE代表一個(gè)能夠存放String類型的ThreadLocal對象训措。此時(shí)不論什么一個(gè)線程能夠并發(fā)訪問這個(gè)變量伪节,對它進(jìn)行寫入、讀取操作绩鸣,都是線程安全的怀大。

(3)實(shí)現(xiàn)解析



上面先取到當(dāng)前線程,然后調(diào)用 getMap 方法獲取對應(yīng)的 ThreadLocalMap呀闻,ThreadLocalMap 是 ThreadLocal 的靜態(tài)內(nèi)部類化借,然后 Thread 類中有一個(gè)這樣類型成員,所以 getMap 是直接返回 Thread 的成員捡多”涂担看下 ThreadLocal 的內(nèi)部類 ThreadLocalMap 源碼:


可以看到有個(gè) Entry 內(nèi)部靜態(tài)類铐炫,它繼承了 WeakReference,總之它記錄了兩個(gè)信息钓瞭,一個(gè)是 ThreadLocal<?>類型驳遵,一個(gè)是 Object 類型的值淫奔。getEntry 方法則是獲取某個(gè) ThreadLocal 對應(yīng)的值山涡,set 方法就是更新或賦值相應(yīng)的 ThreadLocal對應(yīng)的值。

回顧我們的 get 方法唆迁,其實(shí)就是拿到每個(gè)線程獨(dú)有的 ThreadLocalMap然后再用 ThreadLocal 的當(dāng)前實(shí)例鸭丛,拿到 Map 中的相應(yīng)的 Entry,然后就可以拿到相應(yīng)的值返回出去唐责。當(dāng)然鳞溉,如果 Map 為空,還會先進(jìn)行 map 的創(chuàng)建鼠哥,初始化等工作熟菲。

(4)引發(fā)的內(nèi)存泄漏分析

預(yù)備知識

引用

Object o = new Object();

這個(gè) o,我們可以稱之為對象引用朴恳,而 new Object()我們可以稱之為在內(nèi)存中產(chǎn)生了一個(gè)對象實(shí)例抄罕。

當(dāng)寫下 o=null 時(shí),只是表示 o 不再指向堆中 object 的對象實(shí)例于颖,不代表這個(gè)對象實(shí)例不存在了呆贿。

強(qiáng)引用就是指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用森渐,只要強(qiáng)引用還存在做入,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象實(shí)例。

軟引用是用來描述一些還有用但并非必需的對象同衣。對于軟引用關(guān)聯(lián)著的對象竟块,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象實(shí)例列進(jìn)回收范圍之中進(jìn)行第二次回收耐齐。如果這次回收還沒有足夠的內(nèi)存浪秘,才會拋出內(nèi)存溢出異常。在 JDK1.2 之后蚪缀,提供了 SoftReference 類來實(shí)現(xiàn)軟引用秫逝。

弱引用也是用來描述非必需對象的,但是它的強(qiáng)度比軟引用更弱一些询枚,被弱引用關(guān)聯(lián)的對象實(shí)例只能生存到下一次垃圾收集發(fā)生之前违帆。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠金蜀,都會回收掉只被弱引用關(guān)聯(lián)的對象實(shí)例刷后。在 JDK 1.2 之后的畴,提供了 WeakReference 類來實(shí)現(xiàn)弱引用。

虛引用也稱為幽靈引用或者幻影引用尝胆,它是最弱的一種引用關(guān)系丧裁。一個(gè)對象實(shí)例是否有虛引用的存在,完全不會對其生存時(shí)間構(gòu)成影響含衔,也無法通過虛引用來取得一個(gè)對象實(shí)例煎娇。為一個(gè)對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對象實(shí)例被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在 JDK 1.2 之后贪染,提供了PhantomReference 類來實(shí)現(xiàn)虛引用缓呛。內(nèi)存泄漏的現(xiàn)象執(zhí)行 cn.enjoyedu.ch1.threadlocal 下的 ThreadLocalOOM,并將堆內(nèi)存大小設(shè)置為-Xmx256m杭隙,我們啟用一個(gè)線程池哟绊,大小固定為 5 個(gè)線程

場景 1,首先任務(wù)中不執(zhí)行任何有意義的代碼痰憎,當(dāng)所有的任務(wù)提交執(zhí)行完成后票髓,可以看見,我們這個(gè)應(yīng)用的內(nèi)存占用基本上為 25M 左右

場景 2铣耘,然后我們只簡單的在每個(gè)任務(wù)中 new 出一個(gè)數(shù)組洽沟,執(zhí)行完成后我們可以看見,內(nèi)存占用基本和場景 1 同

場景 3涡拘,當(dāng)我們啟用了 ThreadLocal 以后:執(zhí)行完成后我們可以看見玲躯,內(nèi)存占用變?yōu)榱?100M 左右

場景 4,于是鳄乏,我們加入一行代碼跷车,再執(zhí)行,看看內(nèi)存情況:可以看見橱野,內(nèi)存占用基本和場景 1 同朽缴。

這就充分說明,場景 3水援,當(dāng)我們啟用了 ThreadLocal 以后確實(shí)發(fā)生了內(nèi)存泄漏密强。


分析

根據(jù)我們前面對 ThreadLocal 的分析,我們可以知道每個(gè) Thread 維護(hù)一個(gè)ThreadLocalMap蜗元,這個(gè)映射表的 key 是 ThreadLocal 實(shí)例本身或渤,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值奕扣,它只是作為一個(gè) key來讓線程從 ThreadLocalMap 獲取 value薪鹦。仔細(xì)觀察 ThreadLocalMap,這個(gè) map是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時(shí)會被回收池磁。因此使用了 ThreadLocal 后奔害,引用鏈如圖所示

圖中的虛線表示弱引用。這樣地熄,當(dāng)把 threadlocal 變量置為 null 以后华临,沒有任何強(qiáng)引用指向 threadlocal實(shí)例,所以 threadlocal 將會被 gc 回收端考。這樣一來雅潭,ThreadLocalMap 中就會出現(xiàn)key 為 null 的 Entry,就沒有辦法訪問這些 key 為 null 的 Entry 的 value跛梗,如果當(dāng)前線程再遲遲不結(jié)束的話寻馏,這些 key 為 null 的 Entry 的 value 就會一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value棋弥,而這塊 value 永遠(yuǎn)不會被訪問到了核偿,所以存在著內(nèi)存泄露。只有當(dāng)前 thread 結(jié)束以后顽染,current thread 就不會存在棧中漾岳,強(qiáng)引用斷開,Current Thread粉寞、Map value 將全部被 GC 回收尼荆。最好的做法是不在需要使用ThreadLocal 變量后,都調(diào)用它的 remove()方法唧垦,清除數(shù)據(jù)捅儒。所以回到我們前面的實(shí)驗(yàn)場景,場景 3 中振亮,雖然線程池里面的任務(wù)執(zhí)行完畢了巧还,但是線程池里面的 5 個(gè)線程會一直存在直到 JVM 退出,我們 set 了線程的localVariable 變量后沒有調(diào)用 localVariable.remove()方法坊秸,導(dǎo)致線程池里面的 5 個(gè)線程的 threadLocals 變量里面的 new LocalVariable()實(shí)例沒有被釋放麸祷。其實(shí)考察 ThreadLocal 的實(shí)現(xiàn),我們可以看見褒搔,無論是 get()阶牍、set()在某些時(shí)候,調(diào)用了 expungeStaleEntry 方法用來清除 Entry 中 Key 為 null 的 Value星瘾,但是這是不及時(shí)的走孽,也不是每次都會執(zhí)行的,所以一些情況下還是會發(fā)生內(nèi)存泄露琳状。只有 remove()方法中顯式調(diào)用了 expungeStaleEntry 方法磕瓷。從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但是另一個(gè)問題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用算撮?

下面我們分兩種情況討論:

key 使用強(qiáng)引用:對 ThreadLocal 對象實(shí)例的引用被置為 null 了生宛,但是ThreadLocalMap 還持有這個(gè) ThreadLocal 對象實(shí)例的強(qiáng)引用县昂,如果沒有手動刪除,ThreadLocal 的對象實(shí)例不會被回收陷舅,導(dǎo)致 Entry 內(nèi)存泄漏倒彰。

key 使用弱引用:對 ThreadLocal 對象實(shí)例的引用被被置為 null 了,由于ThreadLocalMap 持有 ThreadLocal 的弱引用莱睁,即使沒有手動刪除待讳,ThreadLocal 的對象實(shí)例也會被回收。value 在下一次 ThreadLocalMap 調(diào)用 set仰剿,get创淡,remove 都有機(jī)會被回收看疙。比較兩種情況望侈,我們可以發(fā)現(xiàn):由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果都沒有手動刪除對應(yīng) key蹦漠,都會導(dǎo)致內(nèi)存泄漏部凑,但是使用弱引用可以多一層保障露乏。因此,ThreadLocal 內(nèi)存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟Thread 一樣長涂邀,如果沒有手動刪除對應(yīng) key 就會導(dǎo)致內(nèi)存泄漏瘟仿,而不是因?yàn)槿跻谩?/p>

總結(jié)

JVM 利用設(shè)置 ThreadLocalMap 的 Key 為弱引用,來避免內(nèi)存泄露比勉。

JVM 利用調(diào)用 remove劳较、get、set 方法的時(shí)候浩聋,回收弱引用观蜗。

當(dāng) ThreadLocal 存儲很多 Key 為 null 的 Entry 的時(shí)候,而不再去調(diào)用 remove赡勘、get嫂便、set 方法,那么將導(dǎo)致內(nèi)存泄漏闸与。

使用線程池+ ThreadLocal 時(shí)要小心毙替,因?yàn)檫@種情況下,線程是一直在不斷的重復(fù)運(yùn)行的践樱,從而也就造成了 value 可能造成累積的情況厂画。

(5)錯(cuò)誤使用 ThreadLocal 導(dǎo)致線程不安全

參見代碼 cn.enjoyedu.ch1.threadlocal. ThreadLocalUnsafe

運(yùn)行后的結(jié)果為

如果我們加入 SleepTools.ms(2);會看的更明顯

為什么每個(gè)線程都輸出 5?難道他們沒有獨(dú)自保存自己的 Number 副本嗎拷邢?

為什么其他線程還是能夠修改這個(gè)值袱院?仔細(xì)考察 ThreadLocal 和 Thead 的代碼,我們發(fā)現(xiàn) ThreadLocalMap 中保存的其實(shí)是對象的一個(gè)引用,這樣的話忽洛,當(dāng)有其他線程對這個(gè)引用指向的對象實(shí)例做修改時(shí)腻惠,其實(shí)也同時(shí)影響了所有的線程持有的對象引用所指向的同一個(gè)對象實(shí)例。這也就是為什么上面的程序?yàn)槭裁磿敵鲆粯拥慕Y(jié)果:5 個(gè)線程中保存的是同一 Number 對象的引用欲虚,在線程睡眠的時(shí)候集灌,其他線程將 num 變量進(jìn)行了修改,而修改的對象 Number 的實(shí)例是同一份复哆,因此它們最終輸出的結(jié)果是相同的欣喧。而上面的程序要正常的工作,應(yīng)該的用法是讓每個(gè)線程中的 ThreadLocal 都應(yīng)該持有一個(gè)新的 Number 對象梯找。?

3唆阿、線程間的協(xié)作

線程之間相互配合,完成某項(xiàng)工作锈锤,比如:一個(gè)線程修改了一個(gè)對象的值驯鳖,而另一個(gè)線程感知到了變化,然后進(jìn)行相應(yīng)的操作牙咏,整個(gè)過程開始于一個(gè)線程臼隔,而最終執(zhí)行又是另一個(gè)線程。前者是生產(chǎn)者妄壶,后者就是消費(fèi)者,這種模式隔離了“做什么”(what)和“怎么做”(How)寄狼,簡單的辦法是讓消費(fèi)者線程不斷地循環(huán)檢查變量是否符合預(yù)期在 while 循環(huán)中設(shè)置不滿足的條件丁寄,如果條件滿足則退出 while 循環(huán),從而完成消費(fèi)者的工作泊愧。卻存在如下問題:

1) 難以確保及時(shí)性伊磺。

2)難以降低開銷。如果降低睡眠的時(shí)間删咱,比如休眠 1 毫秒屑埋,這樣消費(fèi)者能更加迅速地發(fā)現(xiàn)條件變化,但是卻可能消耗更多的處理器資源痰滋,造成了無端的浪費(fèi)摘能。

等待/通知機(jī)制

是指一個(gè)線程 A 調(diào)用了對象 O 的 wait()方法進(jìn)入等待狀態(tài),而另一個(gè)線程 B調(diào)用了對象 O 的 notify()或者 notifyAll()方法敲街,線程 A 收到通知后從對象 O 的 wait()方法返回团搞,進(jìn)而執(zhí)行后續(xù)操作。上述兩個(gè)線程通過對象 O 來完成交互多艇,而對象上的 wait()和 notify/notifyAll()的關(guān)系就如同開關(guān)信號一樣逻恐,用來完成等待方和通知方之間的交互工作。

notify():

通知一個(gè)在對象上等待的線程,使其從 wait 方法返回,而返回的前提是該線程

獲取到了對象的鎖,沒有獲得鎖的線程重新進(jìn)入 WAITING 狀態(tài)复隆。

notifyAll():

通知所有等待在該對象上的線程

wait()

調(diào)用該方法的線程進(jìn)入 WAITING 狀態(tài),只有等待另外線程的通知或被中斷才會返回.需要注意,調(diào)用 wait()方法后,會釋放對象的鎖

wait(long)

超時(shí)等待一段時(shí)間,這里的參數(shù)時(shí)間是毫秒,也就是等待長達(dá)n 毫秒,如果沒有通知就超時(shí)返回

wait (long,int)

對于超時(shí)時(shí)間更細(xì)粒度的控制,可以達(dá)到納秒等待和通知的標(biāo)準(zhǔn)范式等待方遵循如下原則拨匆。

1)獲取對象的鎖。

2)如果條件不滿足挽拂,那么調(diào)用對象的 wait()方法涮雷,被通知后仍要檢查條件。

3)條件滿足則執(zhí)行對應(yīng)的邏輯轻局。

通知方遵循如下原則洪鸭。

1)獲得對象的鎖。

2)改變條件仑扑。

3)通知所有等待在對象上的線程览爵。

在調(diào)用 wait()、notify()系列方法之前镇饮,線程必須要獲得該對象的對象級別鎖蜓竹,即只能在同步方法或同步塊中調(diào)用 wait()方法、notify()系列方法储藐,進(jìn)入 wait()方法后俱济,當(dāng)前線程釋放鎖,在從 wait()返回前钙勃,線程與其他線程競爭重新獲得鎖蛛碌,執(zhí)行 notify()系列方法的線程退出調(diào)用了 notifyAll 的 synchronized代碼塊的時(shí)候后,他們就會去競爭辖源。如果其中一個(gè)線程獲得了該對象鎖蔚携,它就會繼續(xù)往下執(zhí)行,在它退出 synchronized 代碼塊克饶,釋放鎖后酝蜒,其他的已經(jīng)被喚醒的線程將會繼續(xù)競爭獲取該鎖,一直進(jìn)行下去矾湃,直到所有被喚醒的線程都執(zhí)行完畢亡脑。notify 和 notifyAll 應(yīng)該用誰盡可能用 notifyall(),謹(jǐn)慎使用 notify()邀跃,因?yàn)?notify()只會喚醒一個(gè)線程霉咨,我們無法確保被喚醒的這個(gè)線程一定就是我們需要喚醒的線程,具體表現(xiàn)參見代碼:

包 cn.enjoyedu.ch1.wn 下

等待超時(shí)模式實(shí)現(xiàn)一個(gè)連接池

調(diào)用場景:調(diào)用一個(gè)方法時(shí)等待一段時(shí)間(一般來說是給定一個(gè)時(shí)間段)坞嘀,如果該方法能夠在給定的時(shí)間段之內(nèi)得到結(jié)果躯护,那么將結(jié)果立刻返回,反之丽涩,超時(shí)返回默認(rèn)結(jié)果棺滞。假設(shè)等待時(shí)間段是 T裁蚁,那么可以推斷出在當(dāng)前時(shí)間 now+T 之后就會超時(shí)等待

持續(xù)時(shí)間:REMAINING=T。

?超時(shí)時(shí)間:FUTURE=now+T继准。

// 對當(dāng)前對象加鎖

public synchronized Object get(long mills) throws InterruptedException {

long future = System.currentTimeMillis() + mills;

long remaining = mills;

// 當(dāng)超時(shí)大于 0 并且 result 返回值不滿足要求

while ((result == null) && remaining > 0) {

wait(remaining);

remaining = future - System.currentTimeMillis();

}

return result;

}

具體實(shí)現(xiàn)參見:包下 cn.enjoyedu.ch1.pool 的代碼

客戶端獲取連接的過程被設(shè)定為等待超時(shí)的模式枉证,也就是在 1000 毫秒內(nèi)如果無法獲取到可用連接,將會返回給客戶端一個(gè) null移必。設(shè)定連接池的大小為 10個(gè)室谚,然后通過調(diào)節(jié)客戶端的線程數(shù)來模擬無法獲取連接的場景。它通過構(gòu)造函數(shù)初始化連接的最大上限崔泵,通過一個(gè)雙向隊(duì)列來維護(hù)連接秒赤,調(diào)用方需要先調(diào)用 fetchConnection(long)方法來指定在多少毫秒內(nèi)超時(shí)獲取連接,當(dāng)連接使用完成后憎瘸,需要調(diào)用 releaseConnection(Connection)方法將連接放回線程池

四入篮、面試題

調(diào)用 yield() 、sleep()幌甘、wait()潮售、notify()等方法對鎖有何影響?

yield() 锅风、sleep()被調(diào)用后酥诽,都不會釋放當(dāng)前線程所持有的鎖。

調(diào)用 wait()方法后皱埠,會釋放當(dāng)前線程持有的鎖肮帐,而且當(dāng)前被喚醒后,會重新去競爭鎖漱逸,鎖競爭到后才會執(zhí)行 wait 方法后面的代碼泪姨。

調(diào)用 notify()系列方法后,對鎖無影響饰抒,線程只有在 syn 同步代碼執(zhí)行完后才會自然而然的釋放鎖,所以 notify()系列方法一般都是 syn 同步代碼的最后一行诀黍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末袋坑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子眯勾,更是在濱河造成了極大的恐慌枣宫,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吃环,死亡現(xiàn)場離奇詭異也颤,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)郁轻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門翅娶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來文留,“玉大人,你說我怎么就攤上這事竭沫≡锍幔” “怎么了?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵蜕提,是天一觀的道長森书。 經(jīng)常有香客問我,道長谎势,這世上最難降的妖魔是什么凛膏? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮脏榆,結(jié)果婚禮上猖毫,老公的妹妹穿的比我還像新娘。我一直安慰自己姐霍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布恨胚。 她就那樣靜靜地躺著,像睡著了一般俄烁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音臭增,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的逃魄。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼饵溅,長吁一口氣:“原來是場噩夢啊……” “哼蜕企!你這毒婦竟也來了幸乒?” 一聲冷哼從身側(cè)響起奋构,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤根灯,失蹤者是張志新(化名)和其女友劉穎纳猪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闪檬,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡样傍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铺遂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡娃循,死狀恐怖炕檩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捌斧,我是刑警寧澤笛质,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站捞蚂,受9級特大地震影響妇押,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜姓迅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一敲霍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丁存,春花似錦肩杈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至聋伦,卻和暖如春夫偶,著一層夾襖步出監(jiān)牢的瞬間界睁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工兵拢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翻斟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓说铃,卻偏偏與公主長得像访惜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子截汪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評論 2 359