基礎(chǔ)概念
什么是進(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)行活動(dòng),進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位。
進(jìn)程是程序在計(jì)算機(jī)上的一次執(zhí)行活動(dòng)也榄。當(dāng)你運(yùn)行一個(gè)程序,你就啟動(dòng)了一 個(gè)進(jìn)程巡莹。顯然,程序是死的、靜態(tài)的,進(jìn)程是活的甜紫、動(dòng)態(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)程就是所有由你啟動(dòng)的進(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)程所擁有的全部資源拓型。
線程無(wú)處不在
任何一個(gè)程序都必須要?jiǎng)?chuàng)建線程,特別是 Java 不管任何程序都必須啟動(dòng)一個(gè) main 函數(shù)的主線程; Java Web 開發(fā)里面的定時(shí)任務(wù)额嘿、定時(shí)器、JSP 和 Servlet劣挫、異 步消息處理機(jī)制,遠(yuǎn)程訪問接口RM等,任何一個(gè)監(jiān)聽事件, onclick的觸發(fā)事件等都 離不開線程和并發(fā)的知識(shí)册养。
CPU 核心數(shù)和線程數(shù)的關(guān)系
多核心:也指單芯片多處理器(Chip Multiprocessors,簡(jiǎn)稱 CMP),CMP是由美國(guó)斯坦福大學(xué)提出的,其思想是將大規(guī)模并行處理器中的 SMP(對(duì)稱多處理器)集成到同一芯片內(nèi),各個(gè)處理器并行執(zhí)行不同的進(jìn)程。這種依靠多個(gè)CPU同時(shí)并行地運(yùn)行程序是實(shí)現(xiàn)超高速計(jì)算的一個(gè)重要方向,稱為并行處理
多線程: Simultaneous Multithreading.簡(jiǎn)稱 SMT.讓同一個(gè)處理器上的多個(gè)線 程同步執(zhí)行并共享處理器的執(zhí)行資源压固。
核心數(shù)球拦、線程數(shù):目前主流CPU都是多核的。增加核心數(shù)目就是為了增加線程數(shù),因?yàn)椴僮飨到y(tǒng)是通過線程來執(zhí)行任務(wù)的,一般情況下它們是 1:1 對(duì)應(yīng)關(guān)系,也就是說四核CPU 一般擁有四個(gè)線程帐我。但I(xiàn)ntel引入超線程技術(shù)后,使核心數(shù)與線程 數(shù)形成 1:2 的關(guān)系
CPU 時(shí)間片輪轉(zhuǎn)機(jī)制
我們平時(shí)在開發(fā)的時(shí)候刘莹,感覺并沒有受 cpu 核心數(shù)的限制,想啟動(dòng)線程就啟 動(dòng)線程焚刚,哪怕是在單核 CPU 上点弯,為什么?這是因?yàn)椴僮飨到y(tǒng)提供了一種 CPU 時(shí) 間片輪轉(zhuǎn)機(jī)制矿咕。
時(shí)間片輪轉(zhuǎn)調(diào)度是一種最古老抢肛、最簡(jiǎn)單、最公平且使用最廣的算法,又稱 RR 調(diào)度碳柱。每個(gè)進(jìn)程被分配一個(gè)時(shí)間段,稱作它的時(shí)間片,即該進(jìn)程允許運(yùn)行的時(shí)間捡絮。
百度百科對(duì) 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í)間片的長(zhǎng)度福稳。從一個(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ī)會(huì)越妈。多數(shù)用戶無(wú)法忍受一條簡(jiǎn)短命令要 5 才能 做出響應(yīng),同樣的問題在一臺(tái)支持多道程序的個(gè)人計(jì)算機(jī)上也會(huì)發(fā)
結(jié)論可以歸結(jié)如下:時(shí)間片設(shè)得太短會(huì)導(dǎo)致過多的進(jìn)程切換,降低了 CPU 效率: 而設(shè)得太長(zhǎng)又可能引起對(duì)短的交互請(qǐng)求的響應(yīng)變差季俩。將時(shí)間片設(shè)為 100ms 通常 是一個(gè)比較合理的折衷。
在 CPU 死機(jī)的情況下,其實(shí)大家不難發(fā)現(xiàn)當(dāng)運(yùn)行一個(gè)程序的時(shí)候把 CPU 給弄 到了 100%再不重啟電腦的情況下,其實(shí)我們還是有機(jī)會(huì)把它 KⅢ掉的,我想也正是 因?yàn)檫@種機(jī)制的緣故梅掠。
澄清并行和并發(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ù)?strong>并發(fā)的時(shí)候一定要加個(gè)單位時(shí)間,也就是說單位時(shí)間內(nèi)并發(fā)量是多少? 離開了單位時(shí)間其實(shí)是沒有意義的阎抒。
俗話說,一心不能二用,這對(duì)計(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ī)的速度太 快,我們無(wú)法察覺到而已.
并行:指應(yīng)用能夠同時(shí)執(zhí)行不同的任務(wù),例:吃飯的時(shí)候可以邊吃飯邊打電話, 這兩件事情可以同時(shí)執(zhí)行
兩者區(qū)別:一個(gè)是交替執(zhí)行,一個(gè)是同時(shí)執(zhí)行.
高并發(fā)編程的意義祭示、好處和注意事項(xiàng)
由于多核多線程的 CPU 的誕生,多線程、高并發(fā)的編程越來越受重視和關(guān)注谴古。 多線程可以給程序帶來如下好處。
- 充分利用 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í)坐地鐵一樣,很多人坐長(zhǎng)線地鐵的時(shí)候都在認(rèn)真看書,而不是為 了坐地鐵而坐地鐵,到家了再去看書,這樣你的時(shí)間就相當(dāng)于有了兩倍。這就是為 什么有些人時(shí)間很充裕,而有些人老是說沒時(shí)間的一個(gè)原因,工作也是這樣,有的 時(shí)候可以并發(fā)地去做幾件事情,充分利用我們的時(shí)間,CPU 也是一樣,也要充分利用怒炸。
- 加快響應(yīng)用戶的時(shí)間
比如我們經(jīng)常用的迅雷下載,都喜歡多開幾個(gè)線程去下載,誰(shuí)都不愿意用一個(gè) 線程去下載,為什么呢?答案很簡(jiǎn)單,就是多個(gè)線程下載快啊带饱。
我們?cè)谧龀绦蜷_發(fā)的時(shí)候更應(yīng)該如此,特別是我們做互聯(lián)網(wǎng)項(xiàng)目,網(wǎng)頁(yè)的響應(yīng) 時(shí)間若提升 1s,如果流量大的話,就能增加不少轉(zhuǎn)換量。做過高性能 web 前端調(diào)優(yōu) 的都知道,要將靜態(tài)資源地址用兩三個(gè)子域名去加載,為什么?因?yàn)槊慷嘁粋€(gè)子域 名,瀏覽器在加載你的頁(yè)面的時(shí)候就會(huì)多開幾個(gè)線程去加載你的頁(yè)面資源,提升網(wǎng) 站的響應(yīng)速度阅羹。多線程,高并發(fā)真的是無(wú)處不在勺疼。
- 可以使你的代碼模塊化,異步化,簡(jiǎn)單化
例如我們實(shí)現(xiàn)電商系統(tǒng),下訂單和給用戶發(fā)送短信捏鱼、郵件就可以進(jìn)行拆分执庐, 將給用戶發(fā)送短信、郵件這兩個(gè)步驟獨(dú)立為單獨(dú)的模塊导梆,并交給其他線程去執(zhí)行轨淌。 這樣既增加了異步的操作,提升了系統(tǒng)性能看尼,又使程序模塊化,清晰化和簡(jiǎn)單化递鹉。
多線程應(yīng)用開發(fā)的好處還有很多,大家在日后的代碼編寫過程中可以慢慢體 會(huì)它的魅力绩鸣。
多線程程序需要注意事項(xiàng)
- 線程之間的安全性
從前面的章節(jié)中我們都知道,在同一個(gè)進(jìn)程里面的多線程是資源共享的,也就 是都可以訪問同一個(gè)內(nèi)存地址當(dāng)中的一個(gè)變量拓劝。例如:若每個(gè)線程中對(duì)全局變量襟企、 靜態(tài)變量只有讀操作,而無(wú)寫操作,一般來說,這個(gè)全局變量是線程安全的:若有多 個(gè)線程同時(shí)執(zhí)行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全讹语。
- 線程之間的死鎖
為了解決線程之間的安全性引入了 Java 的鎖機(jī)制,而一不小心就會(huì)產(chǎn)生 Java 線程死鎖的多線程問題,因?yàn)椴煌木€程都在等待那些根本不可能被釋放的鎖,從而導(dǎo)致所有的工作都無(wú)法完成蒜焊。假設(shè)有兩個(gè)線程,分別代表兩個(gè)饑餓的人,他們必 須共享刀叉并輪流吃飯。他們都需要獲得兩個(gè)鎖:共享刀和共享叉的鎖独泞。
假如線程 A 獲得了刀,而線程 B 獲得了叉亥至。線程 A 就會(huì)進(jìn)入阻塞狀態(tài)來等待 獲得叉,而線程 B 則阻塞來等待線程 A 所擁有的刀。這只是人為設(shè)計(jì)的例子,但盡 管在運(yùn)行時(shí)很難探測(cè)到,這類情況卻時(shí)常發(fā)生
- 線程太多了會(huì)將服務(wù)器資源耗盡形成死機(jī)當(dāng)機(jī)
線程數(shù)太多有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存以及 CPU 的“過渡切換”,造成系統(tǒng)的死機(jī),那么我們?cè)撊绾谓鉀Q這類問題呢?
某些系統(tǒng)資源是有限的,如文件描述符禀挫。多線程程序可能耗盡資源,因?yàn)槊總€(gè) 線程都可能希望有一個(gè)這樣的資源旬陡。如果線程數(shù)相當(dāng)大,或者某個(gè)資源的侯選線 程數(shù)遠(yuǎn)遠(yuǎn)超過了可用的資源數(shù)則最好使用資源池。一個(gè)最好的示例是數(shù)據(jù)庫(kù)連接 池语婴。只要線程需要使用一個(gè)數(shù)據(jù)庫(kù)連接,它就從池中取出一個(gè),使用以后再將它返 回池中描孟。資源池也稱為資源庫(kù)。
多線程應(yīng)用開發(fā)的注意事項(xiàng)很多,希望大家在日后的工作中可以慢慢體會(huì)它 的危險(xiǎn)所在砰左。
認(rèn)識(shí) Java 里的線程
Java 程序天生就是多線程的
一個(gè) Java 程序從main()
方法開始執(zhí)行匿醒,然后按照既定的代碼邏輯執(zhí)行,看似沒有其他線程參與缠导,但實(shí)際上 Java 程序天生就是多線程程序廉羔,因?yàn)閳?zhí)行main()
方法的是一個(gè)名稱為main
的線程。
public class OnlyMain {
public static void main(String[] args) {
//Java 虛擬機(jī)線程系統(tǒng)的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要獲取同步的monitor和synchronizer信息僻造,僅僅獲取線程和線程堆棧信息
ThreadInfo[] threadInfos =
threadMXBean.dumpAllThreads(false, false);
// 遍歷線程信息憋他,僅打印線程ID和線程名稱信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] "
+ threadInfo.getThreadName());
}
}
}
[6] Monitor Ctrl-Break //監(jiān)控 Ctrl-Break 中斷信號(hào)的
[5] Attach Listener //內(nèi)存 dump,線程 dump髓削,類信息統(tǒng)計(jì)竹挡,獲取系統(tǒng)屬性等
[4] Signal Dispatcher // 分發(fā)處理發(fā)送給 JVM 信號(hào)的線程
[3] Finalizer // 調(diào)用對(duì)象 finalize 方法的線程
[2] Reference Handler //清除 Reference 的線程
[1] main //main 線程,用戶程序入口
線程的狀態(tài)
Java中線程的狀態(tài)分為6種:
- 初始(
NEW
):新創(chuàng)建了一個(gè)線程對(duì)象立膛,但還沒有調(diào)用start()
方法揪罕。 - 運(yùn)行(
RUNNABLE
):Java線程中將就緒(ready
)和運(yùn)行中(running
)兩種狀態(tài)籠統(tǒng)的稱為“運(yùn)行”。
線程對(duì)象創(chuàng)建后宝泵,其他線程(比如main
線程)調(diào)用了該對(duì)象的start()
方法好啰。該狀態(tài)的線程位于可運(yùn)行線程池中,等待被線程調(diào)度選中儿奶,獲取CPU
的使用權(quán)框往,此時(shí)處于就緒狀態(tài)(read
y)。就緒狀態(tài)的線程在獲得CPU
時(shí)間片后變?yōu)檫\(yùn)行中狀態(tài)(running
)闯捎。 - 阻塞(
BLOCKED
):表示線程阻塞于鎖搅窿。 - 等待(
WAITING
):進(jìn)入該狀態(tài)的線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷)。阻塞和等待的區(qū)別在于隙券,阻塞是被動(dòng)的男应,它是在等待獲取
monitor lock
。而等待是主動(dòng)的娱仔,通過調(diào)用Object.wait()
等方法進(jìn)入沐飘。 - 超時(shí)等待(
TIMED_WAITING
):該狀態(tài)不同于WAITING
,它無(wú)需等待其它線程顯式地喚醒,在一定時(shí)間之后會(huì)被系統(tǒng)自動(dòng)喚醒耐朴。調(diào)用
Thread.sleep()
方法使線程進(jìn)入限期等待狀態(tài)時(shí)借卧,常常用“使一個(gè)線程睡眠”進(jìn)行描述。調(diào)用Object.wait()
方法使線程進(jìn)入限期等待或者無(wú)限期等待時(shí)筛峭,常常用“掛起一個(gè)線程”進(jìn)行描述铐刘。睡眠和掛起是用來描述行為,而阻塞和等待用來描述狀態(tài)影晓。 - 終止(
TERMINATED
):表示該線程已經(jīng)執(zhí)行完畢镰吵。
狀態(tài)之間的變遷如下圖所示
線程的啟動(dòng)與中止
啟動(dòng)
啟動(dòng)線程的方式有:
- X extends Thread;,然后 X.start
- X implements Runnable挂签;然后交給 Thread 運(yùn)行
public class NewThread {
/*擴(kuò)展自Thread類*/
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
// do my work;
System.out.println("I am extendec Thread");
}
}
/*實(shí)現(xiàn)Runnable接口*/
private static class UseRunnable implements Runnable{
@Override
public void run() {
// do my work;
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
useThread.start();
useThread.start();
UseRunnable useRunnable = new UseRunnable();
new Thread(useRunnable).start();
}
}
Thread 和 Runnable 的區(qū)別
Thread 才是 Java 里對(duì)線程的唯一抽象疤祭,Runnable 只是對(duì)任務(wù)(業(yè)務(wù)邏輯) 的抽象。Thread 可以接受任意一個(gè) Runnable 的實(shí)例并執(zhí)行饵婆。
中止
線程自然終止
要么是 run 執(zhí)行完成了勺馆,要么是拋出了一個(gè)未處理的異常導(dǎo)致線程提前結(jié)束。stop
暫停侨核、恢復(fù)和停止操作對(duì)應(yīng)在線程 Thread 的 API 就是suspend()草穆、resume() 和 stop()。但是這些 API 是過期的搓译,也就是不建議使用的悲柱。不建議使用的原因主 要有:以suspend()
方法為例,在調(diào)用后侥衬,線程不會(huì)釋放已經(jīng)占有的資源(比如 鎖),而是占有著資源進(jìn)入睡眠狀態(tài)跑芳,這樣容易引發(fā)死鎖問題轴总。同樣,stop()
方 法在終結(jié)一個(gè)線程時(shí)不會(huì)保證線程的資源正常釋放博个,通常是沒有給予線程完成資 源釋放工作的機(jī)會(huì)怀樟,因此會(huì)導(dǎo)致程序可能工作在不確定狀態(tài)下。正因?yàn)?code>suspend()盆佣、resume()
和stop()
方法帶來的副作用往堡,這些方法才被標(biāo)注為不建議使用的過期方 法。中斷
安全的中止則是其他線程通過調(diào)用某個(gè)線程 A 的interrupt()
方法對(duì)其進(jìn)行中 斷操作, 中斷好比其他線程對(duì)該線程打了個(gè)招呼共耍,“A虑灰,你要中斷了”,不代表 線程 A 會(huì)立即停止自己的工作痹兜,同樣的 A 線程完全可以不理會(huì)這種中斷請(qǐng)求穆咐。 因?yàn)?java 里的線程是協(xié)作式的,不是搶占式的。線程通過檢查自身的中斷標(biāo)志 位是否被置為 true 來進(jìn)行響應(yīng)对湃,
中斷
InterruptedException
通過調(diào)用一個(gè)線程的interrupt()
來中斷該線程崖叫,如果該線程處于阻塞、限期等待或者無(wú)限期等待狀態(tài)拍柒,那么就會(huì)拋出InterruptedException
心傀,從而提前結(jié)束該線程。但是不能中斷I/O阻塞和synchronized
鎖阻塞拆讯。
對(duì)于以下代碼脂男,在main()
中啟動(dòng)一個(gè)線程之后再中斷它,由于線程中調(diào)用了Thread.sleep()
方法往果,因此會(huì)拋出一個(gè)InterruptedException
疆液,從而提前結(jié)束線程,不執(zhí)行之后的語(yǔ)句陕贮。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
線程通過方法isInterrupted()
來進(jìn)行判斷是否被中斷堕油,也可以調(diào)用靜態(tài)方法Thread.interrupted()
來進(jìn)行判斷當(dāng)前線程是否被中斷,不過Thread.interrupted()
會(huì)同時(shí)將中斷標(biāo)識(shí)位改寫為 false肮之。
isInterrupted()
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start interrupt flag = "+isInterrupted());
while (!isInterrupted()){
System.out.println(threadName + " is running");
System.out.println(threadName + " inner interrupt flag = "+isInterrupted());
}
System.out.println(threadName + " end interrupt flag = "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new UseThread();
thread.start();
Thread.sleep(20);
thread.interrupt();
}
Thread-0 start interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
......
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 end interrupt flag = true
interrupted()
private static class UseThread extends Thread{
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " start interrupt flag = "+isInterrupted());
while (!Thread.interrupted()){
System.out.println(threadName + " is running");
System.out.println(threadName + " inner interrupt flag = "+isInterrupted());
}
System.out.println(threadName + " end interrupt flag = "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new UseThread();
thread.start();
Thread.sleep(20);
thread.interrupt();
}
Thread-0 start interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
......
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 is running
Thread-0 inner interrupt flag = false
Thread-0 end interrupt flag = false
如果一個(gè)線程處于了阻塞狀態(tài)(如線程調(diào)用了thread.sleep
掉缺、thread.join
、thread.wait
等)戈擒,則在線程在檢查中斷標(biāo)示時(shí)如果發(fā)現(xiàn)中斷標(biāo)示為true
眶明,則會(huì)在這些阻塞方法調(diào)用處拋出InterruptedException
異常,并且在拋出異常后會(huì)立即將線程的中斷標(biāo)示位清除筐高,即重新設(shè)置為false
搜囱。
不建議自定義一個(gè)取消標(biāo)志位來中止線程的運(yùn)行。因?yàn)?code>run方法里有阻塞調(diào) 用時(shí)會(huì)無(wú)法很快檢測(cè)到取消標(biāo)志柑土,線程必須從阻塞調(diào)用返回后蜀肘,才會(huì)檢查這個(gè)取 消標(biāo)志。這種情況下稽屏,使用中斷會(huì)更好扮宠,因?yàn)?
- 一般的阻塞方法,如
sleep
等本身就支持中斷的檢查狐榔, - 檢查中斷位的狀態(tài)和檢查取消標(biāo)志位沒什么區(qū)別坛增,用中斷位的狀態(tài)還可 以避免聲明取消標(biāo)志位,減少資源的消耗薄腻。
注意:處于死鎖狀態(tài)的線程無(wú)法被中斷
中斷異常處理
private static class InterruptedExceptionThread extends Thread{
@Override
public void run() {
while(!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()
+" in InterruptedException interrupt flag is " +isInterrupted());
//資源釋放
// interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName() +" interrupt flag is "+isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
InterruptedExceptionThread thread = new InterruptedExceptionThread();
thread.start();
Thread.sleep(500);
thread.interrupt();
}
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 in InterruptedException interrupt flag is false
Thread-0 I am extends Thread.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.zxj.test.InterruptExample$InterruptedExceptionThread.run(InterruptExample.java:47)
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
......
可以看到發(fā)生中斷異常后收捣,線程還是回繼續(xù)執(zhí)行的。我們可以看到在阻塞方法捕捉到異常后的打印isInterrupted()
為false
庵楷,說明在調(diào)用interrupt()
方法坏晦,捕捉到異常后會(huì)把中斷標(biāo)志位又true
改為false
。如果這種情況,我們確實(shí)要中斷線程怎么辦呢昆婿?只需要在catch里在手動(dòng)調(diào)用interrupt();
方法(打開上面代碼的注釋)球碉。
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 I am extends Thread.
Thread-0 in InterruptedException interrupt flag is false
Thread-0 I am extends Thread.
Thread-0 interrupt flag is true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.zxj.test.InterruptExample$InterruptedExceptionThread.run(InterruptExample.java:47)
對(duì) Java 里的線程再多一點(diǎn)點(diǎn)認(rèn)識(shí)
深入理解 run()和 start()
Thread
類是Java里對(duì)線程概念的抽象,可以這樣理解:我們通過new Thread()
其實(shí)只是new出一個(gè)Thread
的實(shí)例仓蛆,還沒有操作系統(tǒng)中真正的線程掛起鉤來睁冬。 只有執(zhí)行了start()
方法后,才實(shí)現(xiàn)了真正意義上的啟動(dòng)線程看疙。
start()
方法讓一個(gè)線程進(jìn)入就緒隊(duì)列等待分配cpu
豆拨,分到cpu
后才調(diào)用實(shí)現(xiàn) 的 run()
方法,start()
方法不能重復(fù)調(diào)用能庆,如果重復(fù)調(diào)用會(huì)拋出異常施禾。
而run
方法是業(yè)務(wù)邏輯實(shí)現(xiàn)的地方,本質(zhì)上和任意一個(gè)類的任意一個(gè)成員方 法并沒有任何區(qū)別搁胆,可以重復(fù)執(zhí)行弥搞,也可以被單獨(dú)調(diào)用。
其他的線程相關(guān)方法
yield()方法
使當(dāng)前線程讓出CPU
占有權(quán)渠旁,但讓出的時(shí)間是不可設(shè)定的攀例。也不會(huì)釋放鎖資源。注意:并不是每個(gè)線程都需要這個(gè)鎖的顾腊,而且執(zhí)行yield( )
的線程不一定就會(huì)持有鎖粤铭,我們完全可以在釋放鎖后再調(diào)用 yield 方法。
所有執(zhí)行yield()
的線程有可能在進(jìn)入到就緒狀態(tài)后會(huì)被操作系統(tǒng)再次選中 馬上又被執(zhí)行杂靶。
wait()/notify()/notifyAll()
:后面會(huì)單獨(dú)講述
join 方法
把指定的線程加入到當(dāng)前線程梆惯,可以將兩個(gè)交替執(zhí)行的線程合并為順序執(zhí)行。 比如在線程 B 中調(diào)用了線程 A 的 Join()方法吗垮,直到線程 A 執(zhí)行完畢后垛吗,才會(huì)繼續(xù) 執(zhí)行線程 B。
面試題
調(diào)用yield() 抱既、sleep()职烧、wait()扁誓、notify()
等方法對(duì)鎖有何影響防泵?
yield()
、sleep()
被調(diào)用后蝗敢,都不會(huì)釋放當(dāng)前線程所持有的鎖捷泞。
調(diào)用wait()
方法后,會(huì)釋放當(dāng)前線程持有的鎖寿谴,而且當(dāng)前被喚醒后锁右,會(huì)重新去競(jìng)爭(zhēng)鎖,鎖競(jìng)爭(zhēng)到后才會(huì)執(zhí)行wait
方法后面的代碼。
調(diào)用notify()
系列方法后咏瑟,對(duì)鎖無(wú)影響拂到,線程只有在syn
同步代碼執(zhí)行完后才會(huì)自然而然的釋放鎖,所以 notify()
系列方法一般都是syn同步代碼的最后一行码泞。
線程的優(yōu)先級(jí)
在Java
線程中兄旬,通過一個(gè)整型成員變量priority
來控制優(yōu)先級(jí),優(yōu)先級(jí)的范 圍從1~10余寥,在線程構(gòu)建的時(shí)候可以通過 setPriority(int)
方法來修改優(yōu)先級(jí)领铐,默認(rèn)優(yōu)先級(jí)是5,優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量要多于優(yōu)先級(jí)低的線程宋舷。
設(shè)置線程優(yōu)先級(jí)時(shí)绪撵,針對(duì)頻繁阻塞(休眠或者I/O操作)的線程需要設(shè)置較高優(yōu)先級(jí),而偏重計(jì)算(需要較多CPU時(shí)間或者偏運(yùn)算)的線程則設(shè)置較低的優(yōu)先級(jí)祝蝠,確保處理器不會(huì)被獨(dú)占音诈。在不同的JVM
以及操作系統(tǒng)上,線程規(guī)劃會(huì)存在差異续膳,有些操作系統(tǒng)甚至?xí)雎詫?duì)線程優(yōu)先級(jí)的設(shè)定改艇。
守護(hù)線程
Daemon(守護(hù))線程是一種支持型線程,因?yàn)樗饕挥米鞒绦蛑泻笈_(tái)調(diào)度以及支持性工作坟岔。這意味著谒兄,當(dāng)一個(gè) Java 虛擬機(jī)中不存在非Daemon
線程的時(shí)候,Java
虛擬機(jī)將會(huì)退出社付〕衅#可以通過調(diào)用Thread.setDaemon(true)
將線程設(shè)置為Daemon
線程。我們一般用不上鸥咖,比如垃圾回收線程就是Daemon
線程燕鸽。
public static void main(String[] args) throws InterruptedException, ExecutionException {
UseThread useThread = new UseThread();
//useThread.setDaemon(true);// 設(shè)置為守護(hù)線程
useThread.start();
Thread.sleep(5);
}
private static class UseThread extends Thread{
@Override
public void run() {
try {
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()+ " interrupt flag is " + isInterrupted());
} finally {
//守護(hù)線程中finally不一定起作用
System.out.println(" .............finally");
}
}
}
上面當(dāng)我們沒有設(shè)置守護(hù)線程時(shí),線程是會(huì)一直運(yùn)行著啼辣,但是當(dāng)我們?cè)O(shè)置了守護(hù)線程時(shí)啊研,隨著main
線程執(zhí)行玩,守護(hù)線程也會(huì)停止掉鸥拧。
Daemon
線程被用作完成支持性工作党远,但是在Java
虛擬機(jī)退出時(shí)Daemon
線程中的finally
塊并不一定會(huì)執(zhí)行。在構(gòu)建Daemon
線程時(shí)富弦,不能依靠finally
塊中 的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯沟娱。
線程間的共享和協(xié)作
線程間的共享
synchronized 內(nèi)置鎖
線程開始運(yùn)行,擁有自己的椡蠊瘢空間济似,就如同一個(gè)腳本一樣矫废,按照既定的代碼一步一步地執(zhí)行,直到終止砰蠢。但是蓖扑,每個(gè)運(yùn)行中的線程,如果僅僅是孤立地運(yùn)行台舱,那么沒有一點(diǎn)兒價(jià)值赵誓,或者說價(jià)值很少,如果多個(gè)線程能夠相互配合完成工作柿赊,包括數(shù)據(jù)之間的共享俩功,協(xié)同處理事情。這將會(huì)帶來巨大的價(jià)值碰声。
Java 支持多個(gè)線程同時(shí)訪問一個(gè)對(duì)象或者對(duì)象的成員變量诡蜓,關(guān)鍵字synchronized
可以修飾方法或者以同步塊的形式來進(jìn)行使用,它主要確保多個(gè)線 程在同一個(gè)時(shí)刻胰挑,只能有一個(gè)線程處于方法或者同步塊中蔓罚,它保證了線程對(duì)變量 訪問的可見性和排他性,又稱為內(nèi)置鎖機(jī)制瞻颂。
對(duì)象鎖和類鎖:
對(duì)象鎖是用于對(duì)象實(shí)例方法豺谈,或者一個(gè)對(duì)象實(shí)例上的,類鎖是用于類的靜態(tài)方法或者一個(gè)類的class
對(duì)象上的贡这。我們知道茬末,類的對(duì)象實(shí)例可以有很多個(gè),但是每個(gè)類只有一個(gè)class
對(duì)象盖矫,所以不同對(duì)象實(shí)例的對(duì)象鎖是互不干擾的丽惭,但是 每個(gè)類只有一個(gè)類鎖。
但是有一點(diǎn)必須注意的是辈双,其實(shí)類鎖只是一個(gè)概念上的東西责掏,并不是真實(shí)存在的,類鎖其實(shí)鎖的是每個(gè)類的對(duì)應(yīng)的class
對(duì)象湃望。類鎖和對(duì)象鎖之間也是互不干擾的换衬。
錯(cuò)誤的加鎖和原因分析
但是當(dāng)我們反編譯這個(gè)類的 class 文件后,可以看到 i++實(shí)際是证芭,
本質(zhì)上是返回了一個(gè)新的Integer
對(duì)象瞳浦。也就是每個(gè)線程實(shí)際加鎖的是不同的Integer
對(duì)象。
ReentrantLock鎖
ReentrantLock
檩帐,一個(gè)可重入的互斥鎖术幔,它具有與使用synchronized方法和語(yǔ)句所訪問的隱式監(jiān)視器鎖相同的一些基本行為和語(yǔ)義另萤,但功能更強(qiáng)大湃密。(重入鎖后面介 紹)
1.Lock接口
Lock
诅挑,鎖對(duì)象。在Java中鎖是用來控制多個(gè)線程訪問共享資源的方式泛源,一般來說拔妥,一個(gè)鎖能夠防止多個(gè)線程同時(shí)訪問共享資源(但有的鎖可以允許多個(gè)線程并發(fā)訪問共享資源,比如讀寫鎖达箍,后面我們會(huì)分析)没龙。在Lock接口出現(xiàn)之前,Java程序是靠synchronized
關(guān)鍵字(后面分析)實(shí)現(xiàn)鎖功能的缎玫,而JAVA SE5.0之后并發(fā)包中新增了Lock
接口用來實(shí)現(xiàn)鎖的功能硬纤,它提供了與synchronized
關(guān)鍵字類似的同步功能,只是在使用時(shí)需要顯式地獲取和釋放鎖赃磨,缺點(diǎn)就是缺少像synchronized
那樣隱式獲取釋放鎖的便捷性筝家,但是卻擁有了鎖獲取與釋放的可操作性,可中斷的獲取鎖以及超時(shí)獲取鎖等多種synchronized
關(guān)鍵字所不具備的同步特性邻辉。
Lock接口的主要方法(還有兩個(gè)方法比較復(fù)雜溪王,暫不介紹):
void lock(): 執(zhí)行此方法時(shí),如果鎖處于空閑狀態(tài)值骇,當(dāng)前線程將獲取到鎖莹菱。相反,如果鎖已經(jīng)被其他線程持有吱瘩,將禁用當(dāng)前線程道伟,直到當(dāng)前線程獲取到鎖。
boolean tryLock(): 如果鎖可用使碾,則獲取鎖皱卓,并立即返回true
,否則返回false.該方法和lock()
的區(qū)別在于部逮,tryLock()
只是"試圖"獲取鎖娜汁,如果鎖不可用,不會(huì)導(dǎo)致當(dāng)前線程被禁用兄朋,當(dāng)前線程仍然繼續(xù)往下執(zhí)行代碼掐禁。而lock()
方法則是一定要獲取到鎖,如果鎖不可用颅和,就一直等待傅事,在未獲得鎖之前,當(dāng)前線程并不繼續(xù)向下執(zhí)行.通常采用如下的代碼形式調(diào)用tryLock()
方法:
void unlock(): 執(zhí)行此方法時(shí),當(dāng)前線程將釋放持有的鎖.鎖只能由持有者釋放峡扩,如果線程并不持有鎖蹭越,卻執(zhí)行該方法,可能導(dǎo)致異常的發(fā)生.Condition newCondition()
:條件對(duì)象教届,獲取等待通知組件响鹃。該組件和當(dāng)前的鎖綁定驾霜,當(dāng)前線程只有獲取了鎖,才能調(diào)用該組件的await()
方法买置,而調(diào)用后粪糙, 當(dāng)前線程將縮放鎖。
2.ReentrantLock的使用
關(guān)于ReentrantLock
的使用很簡(jiǎn)單忿项,只需要顯示調(diào)用蓉冈,獲得同步鎖,釋放同步鎖即可轩触。
ReentrantLock lock = new ReentrantLock(); //參數(shù)默認(rèn)false寞酿,不公平鎖
.....................
lock.lock();
//如果被其它資源鎖定,會(huì)在此等待鎖釋放脱柱,達(dá)到暫停的效果
try {
//操作
} finally {
lock.unlock(); //釋放鎖
}
synchronized和ReentrantLock比較
1. 鎖的實(shí)現(xiàn)
synchronized
是JVM
實(shí)現(xiàn)的熟嫩,而ReentrantLock
是JDK
實(shí)現(xiàn)的。
2. 性能
新版本Java
對(duì)synchronized
進(jìn)行了很多優(yōu)化褐捻,例如自旋鎖等掸茅,synchronized
與ReentrantLock
大致相同。
在JDK1.5中柠逞,
synchronized
是性能低效的昧狮。因?yàn)檫@是一個(gè)重量級(jí)操作,它對(duì)性能最大的影響是阻塞的是實(shí)現(xiàn)板壮,掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成逗鸣,這些操作給系統(tǒng)的并發(fā)性帶來了很大的壓力。相比之下使用Java提供的ReentrankLock
對(duì)象绰精,性能更高一些撒璧。到了JDK1.6,發(fā)生了變化笨使,對(duì)synchronize
加入了很多優(yōu)化措施卿樱,有自適應(yīng)自旋,鎖消除硫椰,鎖粗化繁调,輕量級(jí)鎖,偏向鎖等等靶草。導(dǎo)致在JDK1.6上synchronize
的性能并不比Lock差蹄胰。官方也表示,他們也更支持synchronize
奕翔,在未來的版本中還有優(yōu)化余地裕寨,所以還是提倡在synchronized
能實(shí)現(xiàn)需求的情況下,優(yōu)先考慮使用synchronized
來進(jìn)行同步。
3. 等待可中斷
當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候宾袜,正在等待的線程可以選擇放棄等待捻艳,改為處理其他事情。
ReentrantLock
可中斷试和,而synchronized
不行。
4. 公平鎖
公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí)纫普,必須按照申請(qǐng)鎖的時(shí)間順序來依次獲得鎖阅悍。
synchronized
中的鎖是非公平的,ReentrantLock
默認(rèn)情況下也是非公平的昨稼,但是也可以是公平的节视。
5. 鎖綁定多個(gè)條件
一個(gè)ReentrantLock
可以同時(shí)綁定多個(gè)Condition
對(duì)象。
使用選擇
除非需要使用ReentrantLock
的高級(jí)功能假栓,否則優(yōu)先使用synchronized
寻行。這是因?yàn)?code>synchronized是JVM
實(shí)現(xiàn)的一種鎖機(jī)制,JVM
原生地支持它匾荆,而ReentrantLock
不是所有的JDK
版本都支持。并且使用synchronized
不用擔(dān)心沒有釋放鎖而導(dǎo)致死鎖問題,因?yàn)?code>JVM會(huì)確保鎖的釋放灭忠。
volatile屠橄,最輕量的同步機(jī)制
volatile
保證了不同線程對(duì)這個(gè)變量進(jìn)行操作時(shí)的可見性,即一個(gè)線程修改了某 個(gè)變量的值烤芦,這新值對(duì)其他線程來說是立即可見的举娩。參見代碼:
public class VolatileCase {
private static boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run() {
System.out.println("PrintThread is running.......");
while(!ready);
System.out.println("number = "+number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
不加volatile
時(shí),子線程無(wú)法感知主線程修改了ready
的值构罗,從而不會(huì)退出循環(huán)铜涉,而加了volatile
后,子線程可以感知主線程修改了ready的值遂唧,迅速退出循環(huán)芙代。 但是volatile
不能保證數(shù)據(jù)在多個(gè)線程下同時(shí)寫時(shí)的線程安全,參見代碼:
public class NotSafe {
private volatile long count =0;
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
//count進(jìn)行累加
public void incCount(){
count++;
}
//線程
private static class Count extends Thread{
private NotSafe simplOper;
public Count(NotSafe simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for(int i=0;i<10000;i++){
simplOper.incCount();
}
}
}
public static void main(String[] args) throws InterruptedException {
NotSafe simplOper = new NotSafe();
//啟動(dòng)兩個(gè)線程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);
}
}
volatile 最適用的場(chǎng)景:一個(gè)線程寫盖彭,多個(gè)線程讀链蕊。
ThreadLocal 辨析
與 Synchonized 的比較
ThreadLocal
和Synchonized
都用于解決多線程并發(fā)訪問∶冢可是ThreadLocal
與synchronized
有本質(zhì)的差別滔韵。synchronized
是利用鎖的機(jī)制,使變量或代碼塊在某一時(shí)該僅僅能被一個(gè)線程訪問掌实。而ThreadLocal
為每個(gè)線程都提供了變量的副本陪蜻,使得每個(gè)線程在某一時(shí)間訪問到的并非同一個(gè)對(duì)象,這樣就隔離了多個(gè)線程對(duì)數(shù)據(jù)的數(shù)據(jù)共享贱鼻。
ThreadLocal的使用
ThreadLocal
類接口很簡(jiǎn)單宴卖,只有4個(gè)方法滋将,我們先來了解一下:
void set(Object value)
設(shè)置當(dāng)前線程的線程局部變量的值。
public Object get()
該方法返回當(dāng)前線程所對(duì)應(yīng)的線程局部變量症昏。
public void remove()
將當(dāng)前線程局部變量的值刪除随闽,目的是為了減少內(nèi)存的占用,該方法是JDK 5.0新增的方法肝谭。需要指出的是掘宪,當(dāng)線程結(jié)束后,對(duì)應(yīng)該線程的局部變量將自動(dò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 = new ThreadLocal<String>()
;RESOURCE
代表一個(gè)能夠存放String類型的ThreadLocal
對(duì)象花颗。此時(shí)不論什么一個(gè)線程能夠并發(fā)訪問這個(gè)變量,對(duì)它進(jìn)行寫入惠拭、讀取操作扩劝,都是線程安全的。
實(shí)現(xiàn)解析
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面先取到當(dāng)前線程职辅,然后調(diào)用getMap
方法獲取對(duì)應(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
對(duì)應(yīng)的值皱坛,set
方法就是更新或賦值相應(yīng)的ThreadLocal
對(duì)應(yīng)的值。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
回顧我們的get
方法豆巨,其實(shí)就是拿到每個(gè)線程獨(dú)有的ThreadLocalMap
然后再用ThreadLocal
的當(dāng)前實(shí)例剩辟,拿到Map
中的相應(yīng)的Entry
,然后就可以拿到相應(yīng)的值返回出去。當(dāng)然贩猎,如果Map
為空熊户,還會(huì)先進(jìn)行map
的創(chuàng)建,初始化等工作吭服。
引發(fā)的內(nèi)存泄漏分析
預(yù)備知識(shí)
引用
Object o = new Object();
這個(gè)o
嚷堡,我們可以稱之為對(duì)象引用,而new Object()
我們可以稱之為在內(nèi)存中產(chǎn)生了一個(gè)對(duì)象實(shí)例艇棕。
當(dāng)寫下o=null
時(shí)蝌戒,只是表示o
不再指向堆中object
的對(duì)象實(shí)例,不代表這個(gè)對(duì)象實(shí)例不存在了欠肾。
強(qiáng)引用:就是指在程序代碼之中普遍存在的瓶颠,類似“Object obj=new Object()” 這類的引用拟赊,只要強(qiáng)引用還存在刺桃,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象實(shí)例。
軟引用:是用來描述一些還有用但并非必需的對(duì)象吸祟。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象瑟慈, 在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象實(shí)例列進(jìn)回收范圍之中進(jìn)行 第二次回收屋匕。如果這次回收還沒有足夠的內(nèi)存葛碧,才會(huì)拋出內(nèi)存溢出異常。在 JDK 1.2 之后过吻,提供了 SoftReference 類來實(shí)現(xiàn)軟引用进泼。
弱引用:也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些纤虽,被弱 引用關(guān)聯(lián)的對(duì)象實(shí)例只能生存到下一次垃圾收集發(fā)生之前乳绕。當(dāng)垃圾收集器工作時(shí), 無(wú)論當(dāng)前內(nèi)存是否足夠逼纸,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象實(shí)例洋措。在 JDK 1.2 之 后,提供了 WeakReference 類來實(shí)現(xiàn)弱引用杰刽。
虛引用:也稱為幽靈引用或者幻影引用菠发,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象 實(shí)例是否有虛引用的存在贺嫂,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響滓鸠,也無(wú)法通過虛引用 來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象 實(shí)例被收集器回收時(shí)收到一個(gè)系統(tǒng)通知第喳。在 JDK 1.2 之后哥力,提供了 PhantomReference 類來實(shí)現(xiàn)虛引用。
內(nèi)存泄漏的現(xiàn)象
執(zhí)行下面的ThreadLocalOOM
,并將堆內(nèi)存大小設(shè) 置為-Xmx256m
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];/*5M大小的數(shù)組*/
}
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
new LocalVariable();
System.out.println("use local varaible");
}
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
我們啟用一個(gè)線程池吩跋,大小固定為 5 個(gè)線程
場(chǎng)景 1寞射,首先任務(wù)中不執(zhí)行任何有意義的代碼,當(dāng)所有的任務(wù)提交執(zhí)行完成 后锌钮,可以看見桥温,我們這個(gè)應(yīng)用的內(nèi)存占用基本上為 25M 左右
場(chǎng)景 2,然后我們只簡(jiǎn)單的在每個(gè)任務(wù)中 new 出一個(gè)數(shù)組梁丘,執(zhí)行完成后我們 可以看見侵浸,內(nèi)存占用基本和場(chǎng)景 1 同
場(chǎng)景 3,當(dāng)我們啟用了 ThreadLocal 以后:
執(zhí)行完成后我們可以看見氛谜,內(nèi)存占用變?yōu)榱?100M 左右
場(chǎng)景 4掏觉,于是,我們加入一行代碼值漫,再執(zhí)行澳腹,看看內(nèi)存情況:
可以看見,內(nèi)存占用基本和場(chǎng)景 1 同杨何。
這就充分說明酱塔,場(chǎng)景 3,當(dāng)我們啟用了ThreadLocal
以后確實(shí)發(fā)生了內(nèi)存泄 漏危虱。
分析
根據(jù)我們前面對(duì)ThreadLocal
的分析羊娃,我們可以知道每個(gè)Thread
維護(hù)一個(gè) ThreadLocalMap
,這個(gè)映射表的 key 是 ThreadLocal 實(shí)例本身埃跷,value 是真正需 要存儲(chǔ)的Object
蕊玷,也就是說ThreadLocal
本身并不存儲(chǔ)值,它只是作為一個(gè) key 來讓線程從ThreadLocalMap
獲取value
弥雹。仔細(xì)觀察ThreadLocalMap
垃帅,這個(gè) map 是使用ThreadLocal
的弱引用作為Key
的,弱引用的對(duì)象在GC
時(shí)會(huì)被回收缅糟。
因此使用了ThreadLocal
后挺智,引用鏈如圖所示
圖中的虛線表示弱引用。
這樣窗宦,當(dāng)把threadlocal
變量置為null
以后赦颇,沒有任何強(qiáng)引用指向threadlocal
實(shí)例,所以threadlocal
將會(huì)被gc
回收赴涵。這樣一來媒怯,ThreadLocalMap
中就會(huì)出現(xiàn)key
為null
的Entry
,就沒有辦法訪問這些 key
為null
的Entry
的value
髓窜,如果當(dāng)前線程再遲遲不結(jié)束的話扇苞,這些key
為null
的Entry
的value
就會(huì)一直存在一條強(qiáng) 引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
欺殿,而這塊value
永遠(yuǎn)不會(huì)被訪問到了,所以存在著內(nèi)存泄露鳖敷。
只有當(dāng)前thread
結(jié)束以后脖苏,current thread
就不會(huì)存在棧中,強(qiáng)引用斷開定踱,Current Thread棍潘、Map value
將全部被GC回收。最好的做法是不在需要使用ThreadLocal
變量后崖媚,都調(diào)用它的remove()
方法亦歉,清除數(shù)據(jù)。
所以回到我們前面的實(shí)驗(yàn)場(chǎng)景畅哑,場(chǎng)景 3 中肴楷,雖然線程池里面的任務(wù)執(zhí)行完畢 了,但是線程池里面的 5 個(gè)線程會(huì)一直存在直到JVM
退出荠呐,我們set
了線程的 localVariable
變量后沒有調(diào)用localVariable.remove()
方法赛蔫,導(dǎo)致線程池里面的 5 個(gè) 線程的threadLocals
變量里面的new LocalVariable()
實(shí)例沒有被釋放。
其實(shí)考察ThreadLocal
的實(shí)現(xiàn)直秆,我們可以看見濒募,無(wú)論是get()
鞭盟、set()
在某些時(shí) 候圾结,調(diào)用了expungeStaleEntry
方法用來清除Entry
中Key
為null
的Value
,但是 這是不及時(shí)的齿诉,也不是每次都會(huì)執(zhí)行的筝野,所以一些情況下還是會(huì)發(fā)生內(nèi)存泄露。 只有 remove()
方法中顯式調(diào)用了expungeStaleEntry
方法粤剧。
從表面上看內(nèi)存泄漏的根源在于使用了弱引用歇竟,但是另一個(gè)問題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用?
下面我們分兩種情況討論:
key 使用強(qiáng)引用:引用ThreadLocal
的對(duì)象被回收了抵恋,但是ThreadLocalMap
還持有ThreadLocal
的強(qiáng)引用焕议,如果沒有手動(dòng)刪除,ThreadLocal
的對(duì)象實(shí)例不會(huì) 被回收弧关,導(dǎo)致Entry
內(nèi)存泄漏盅安。
key 使用弱引用:引用的ThreadLocal
的對(duì)象被回收了,由于 ThreadLocalMap 持有ThreadLocal
的弱引用世囊,即使沒有手動(dòng)刪除别瞭,ThreadLocal
的對(duì)象實(shí)例也會(huì)被 回收。value
在下一次ThreadLocalMap
調(diào)用set
株憾,get
蝙寨,remove
都有機(jī)會(huì)被回收晒衩。
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap
的生命周期跟Thread
一樣長(zhǎng)墙歪,如果都沒有手動(dòng)刪除對(duì)應(yīng)key
听系,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障虹菲。
因此跛锌,ThreadLocal
內(nèi)存泄漏的根源是:由于ThreadLocalMap
的生命周期跟Thread
一樣長(zhǎng),如果沒有手動(dòng)刪除對(duì)應(yīng)key
就會(huì)導(dǎo)致內(nèi)存泄漏届惋,而不是因?yàn)槿跻谩?/p>
總結(jié)
JVM利用設(shè)置ThreadLocalMap
的Key為弱引用髓帽,來避免內(nèi)存泄露。
JVM利用調(diào)用remove脑豹、get郑藏、set
方法的時(shí)候,回收弱引用瘩欺。
當(dāng)ThreadLocal
存儲(chǔ)很多Key為null的Entry的時(shí)候必盖,而不再去調(diào)用remove、 get俱饿、set
方法歌粥,那么將導(dǎo)致內(nèi)存泄漏。
使用線程池+ThreadLocal時(shí)要小心拍埠,因?yàn)檫@種情況下失驶,線程是一直在不斷的 重復(fù)運(yùn)行的,從而也就造成了value可能造成累積的情況枣购。
錯(cuò)誤使用 ThreadLocal 導(dǎo)致線程不安全
public class ThreadLocalUnsafe implements Runnable {
public static Number number = new Number(0);
public void run() {
//每個(gè)線程計(jì)數(shù)加一
number.setNum(number.getNum()+1);
//將其存儲(chǔ)到ThreadLocal中
value.set(number);
SleepTools.ms(2);
//輸出num值
System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
}
public static ThreadLocal<Number> value = new ThreadLocal<Number>() {};
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
private static class Number {
public Number(int num) {
this.num = num;
}
private int num;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
@Override
public String toString() {
return "Number [num=" + num + "]";
}
}
}
運(yùn)行后的結(jié)果為
如果我們加入SleepTools.ms(2);
會(huì)看的更明顯
為什么每個(gè)線程都輸出5嬉探?難道他們沒有獨(dú)自保存自己的Number
副本嗎?為什么其他線程還是能夠修改這個(gè)值棉圈?仔細(xì)考察ThreadLocal
和Thead
的代碼涩堤,我們發(fā)現(xiàn)ThreadLocalMap
中保存的其實(shí)是對(duì)象的一個(gè)引用,這樣的話分瘾,當(dāng)有其他線程對(duì)這個(gè)引用指向的對(duì)象實(shí)例做修改時(shí)胎围,其實(shí)也同時(shí)影響了所有的線程持有的對(duì)象引用所指向的同一個(gè)對(duì)象實(shí)例。這也就是為什么上面的程序?yàn)槭裁磿?huì)輸出一樣的結(jié)果:5個(gè)線程中保存的是同一Number
對(duì)象的引用德召,在線程睡眠的時(shí)候白魂,其他線程將num
變量進(jìn)行了修改,而修改的對(duì)象Number
的實(shí)例是同一份氏捞,因此它們最終輸出的結(jié)果是相同的碧聪。
而上面的程序要正常的工作,應(yīng)該的用法是讓每個(gè)線程中的ThreadLocal
都應(yīng)該持有一個(gè)新的Number
對(duì)象液茎。
- 去掉成員變量
Number
的static
public Number number = new Number(0);
- 在創(chuàng)建
ThreadLocal
時(shí)給個(gè)初始值
public static ThreadLocal<Number> value = new ThreadLocal<Number>(){
@Override
protected Number initialValue() {
return new Number(0);
}
};
線程間的協(xié)作
線程之間相互配合逞姿,完成某項(xiàng)工作辞嗡,比如:一個(gè)線程修改了一個(gè)對(duì)象的值,而另一個(gè)線程感知到了變化滞造,然后進(jìn)行相應(yīng)的操作续室,整個(gè)過程開始于一個(gè)線程,而最終執(zhí)行又是另一個(gè)線程谒养。前者是生產(chǎn)者挺狰,后者就是消費(fèi)者,這種模式隔離了“做什么”(what)和“怎么做”(How)买窟,簡(jiǎn)單的辦法是讓消費(fèi)者線程不斷地循環(huán)檢查變量是否符合預(yù)期在while
循環(huán)中設(shè)置不滿足的條件丰泊,如果條件滿足則退出while
循環(huán),從而完成消費(fèi)者的工作始绍。卻存在如下問題:
- 難以確保及時(shí)性瞳购。
- 難以降低開銷。如果降低睡眠的時(shí)間亏推,比如休眠1毫秒学赛,這樣消費(fèi)者能更加迅速地發(fā)現(xiàn)條件變化,但是卻可能消耗更多的處理器資源吞杭,造成了無(wú)端的浪費(fèi)盏浇。
等待/通知機(jī)制
是指一個(gè)線程A調(diào)用了對(duì)象O的wait()
方法進(jìn)入等待狀態(tài),而另一個(gè)線程B調(diào)用了對(duì)象O的notify()
或者notifyAll()
方法芽狗,線程A收到通知后從對(duì)象O的wait()
方法返回绢掰,進(jìn)而執(zhí)行后續(xù)操作。上述兩個(gè)線程通過對(duì)象O來完成交互译蒂,而對(duì)象上的wait()
和notify/notifyAll()
的關(guān)系就如同開關(guān)信號(hào)一樣曼月,用來完成等待方和通知方之間的交互工作谊却。
notify():
通知一個(gè)在對(duì)象上等待的線程,使其從wait
方法返回,而返回的前提是該線程 獲取到了對(duì)象的鎖柔昼,沒有獲得鎖的線程重新進(jìn)入WAITING
狀態(tài)。
notifyAll():
通知所有等待在該對(duì)象上的線程
wait()
調(diào)用該方法的線程進(jìn)入WAITING
狀態(tài),只有等待另外線程的通知或被中斷 才會(huì)返回.需要注意,調(diào)用wait()
方法后,會(huì)釋放對(duì)象的鎖
wait(long)
超時(shí)等待一段時(shí)間,這里的參數(shù)時(shí)間是毫秒,也就是等待長(zhǎng)達(dá)n毫秒,如果沒有 通知就超時(shí)返回
wait (long,int)
對(duì)于超時(shí)時(shí)間更細(xì)粒度的控制,可以達(dá)到納秒
等待和通知的標(biāo)準(zhǔn)范式
等待方遵循如下原則炎辨。
- 獲取對(duì)象的鎖捕透。
- 如果條件不滿足,那么調(diào)用對(duì)象的 wait()方法碴萧,被通知后仍要檢查條件乙嘀。
-
條件滿足則執(zhí)行對(duì)應(yīng)的邏輯。
通知方遵循如下原則破喻。
- 獲得對(duì)象的鎖虎谢。
- 改變條件。
-
通知所有等待在對(duì)象上的線程曹质。
在調(diào)用 wait()婴噩、notify()系列方法之前擎场,線程必須要獲得該對(duì)象的對(duì)象級(jí)別鎖,即只能在同步方法或同步塊中調(diào)用 wait()方法几莽、notify()系列方法迅办,進(jìn)入wait()
方法后,當(dāng)前線程釋放鎖章蚣,在從wait()
返回前站欺,線程與其他線程競(jìng)爭(zhēng)重新獲得鎖,執(zhí)行notify()
系列方法的線程退出調(diào)用了notifyAll
的synchronized
代碼塊的時(shí)候后纤垂,他們就會(huì)去競(jìng)爭(zhēng)矾策。如果其中一個(gè)線程獲得了該對(duì)象鎖,它就會(huì)繼續(xù)往下執(zhí)行峭沦,在它退出synchronized
代碼塊蝴韭,釋放鎖后,其他的已經(jīng)被喚醒的線程將會(huì)繼續(xù)競(jìng)爭(zhēng)獲取該鎖熙侍,一直進(jìn)行下去榄鉴,直到所有被喚醒的線程都執(zhí)行完畢。
notify 和 notifyAll 應(yīng)該用誰(shuí)
盡可能用notifyall()
蛉抓,謹(jǐn)慎使用notify()
庆尘,因?yàn)?code>notify()只會(huì)喚醒一個(gè)線程,我們無(wú)法確保被喚醒的這個(gè)線程一定就是我們需要喚醒的線程巷送。
等待超時(shí)模式實(shí)現(xiàn)一個(gè)連接池
調(diào)用場(chǎng)景:調(diào)用一個(gè)方法時(shí)等待一段時(shí)間(一般來說是給定一個(gè)時(shí)間段)驶忌,如果該方法能夠在給定的時(shí)間段之內(nèi)得到結(jié)果,那么將結(jié)果立刻返回笑跛,反之付魔,超時(shí)返回默認(rèn)結(jié)果。
假設(shè)等待時(shí)間段是T飞蹂,那么可以推斷出在當(dāng)前時(shí)間now+T
之后就會(huì)超時(shí)
等待持續(xù)時(shí)間:REMAINING=T
几苍。
超時(shí)時(shí)間:FUTURE=now+T
。
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;
}
DBPool.java
public class DBPool {
private static LinkedList<Connection> pool = new LinkedList<Connection>();
public DBPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
}
public void releaseConnection(Connection connection) {
if (connection != null) {
//TODO
}
}
// 在mills內(nèi)無(wú)法獲取到連接陈哑,將會(huì)返回null
public Connection fetchConnection(long mills) throws InterruptedException {
//TODO
return null;
}
}
DBPoolTest.java
public class DBPoolTest {
static DBPool pool = new DBPool(10);
// 控制器:控制main線程將會(huì)等待所有Woker結(jié)束后才能繼續(xù)執(zhí)行
static CountDownLatch end;
public static void main(String[] args) throws Exception {
// 線程數(shù)量
int threadCount = 50;
end = new CountDownLatch(threadCount);
int count = 20;//每個(gè)線程的操作次數(shù)
AtomicInteger got = new AtomicInteger();//計(jì)數(shù)器:統(tǒng)計(jì)可以拿到連接的線程
AtomicInteger notGot = new AtomicInteger();//計(jì)數(shù)器:統(tǒng)計(jì)沒有拿到連接的線程
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(new Worker(count, got, notGot),
"worker_"+i);
thread.start();
}
end.await();// main線程在此處等待
System.out.println("總共嘗試了: " + (threadCount * count));
System.out.println("拿到連接的次數(shù): " + got);
System.out.println("沒能連接的次數(shù): " + notGot);
}
static class Worker implements Runnable {
int count;
AtomicInteger got;
AtomicInteger notGot;
public Worker(int count, AtomicInteger got,
AtomicInteger notGot) {
this.count = count;
this.got = got;
this.notGot = notGot;
}
public void run() {
while (count > 0) {
try {
// 從線程池中獲取連接妻坝,如果1000ms內(nèi)無(wú)法獲取到,將會(huì)返回null
// 分別統(tǒng)計(jì)連接獲取的數(shù)量got和未獲取到的數(shù)量notGot
Connection connection = pool.fetchConnection(1000);
if (connection != null) {
try {
connection.createStatement();
connection.commit();
} finally {
pool.releaseConnection(connection);
got.incrementAndGet();
}
} else {
notGot.incrementAndGet();
System.out.println(Thread.currentThread().getName()
+"等待超時(shí)!");
}
} catch (Exception ex) {
} finally {
count--;
}
}
end.countDown();
}
}
}
客戶端獲取連接的過程被設(shè)定為等待超時(shí)的模式惊窖,也就是在1000毫秒內(nèi)如果無(wú)法獲取到可用連接刽宪,將會(huì)返回給客戶端一個(gè)null。設(shè)定連接池的大小為10個(gè)界酒,然后通過調(diào)節(jié)客戶端的線程數(shù)來模擬無(wú)法獲取連接的場(chǎng)景圣拄。
它通過構(gòu)造函數(shù)初始化連接的最大上限,通過一個(gè)雙向隊(duì)列來維護(hù)連接毁欣,調(diào)用方需要先調(diào)用fetchConnection(long)
方法來指定在多少毫秒內(nèi)超時(shí)獲取連接庇谆,當(dāng)連接使用完成后赁遗,需要調(diào)用releaseConnection(Connection)
方法將連接放回線程池