一、進(jìn)程與線程
進(jìn)程是資源分配的最小單位,線程是cpu調(diào)度的最小單位。線程也被稱(chēng)為輕量級(jí)進(jìn)程。
所有與進(jìn)程相關(guān)的資源,都被記錄在PCB中
進(jìn)程是搶占處理及的調(diào)度單位;線程屬于某個(gè)進(jìn)程,共享其資源
一個(gè) Java 程序的運(yùn)行是 main 線程和多個(gè)其他線程同時(shí)運(yùn)行。
二、Thread中的start和run方法的區(qū)別
調(diào)用start()方法會(huì)創(chuàng)建一個(gè)新的子線程并啟動(dòng)
run()方法只是Thread的一個(gè)普通方法的調(diào)用,還是在主線程里執(zhí)行。
三、Thread和Runnable是什么關(guān)系?
Thread是實(shí)現(xiàn)了Runnable接口的類(lèi),是的run支持多線程。
因java類(lèi)的單一繼承原則,推薦多使用Runnable接口
四、如何給run()方法傳參?
構(gòu)造函數(shù)傳參
成員變量傳參
回調(diào)函數(shù)傳參
五、如何實(shí)現(xiàn)處理線程的返回值?
實(shí)現(xiàn)的方式主要有三種:
主線程等待法
/*private Stringvalue;public void run() {? ? try {? ? ? ? Thread.currentThread().sleep(5000);? ? } catch (InterruptedException e) {? ? ? ? e.printStackTrace();? ? }? ? value ="we have data now";}*/CycleWait cw = new CycleWait();Thread t = new Thread(cw);t.start();while(cw.value== null){? ? ? ? ? Thread.currentThread().sleep(100);//如果value一直為空,則線程一直sleep? ? ? ? }
使用Thread類(lèi)的join()阻塞當(dāng)前線程,以等待子線程處理完畢
t.join();
通過(guò)Callable接口實(shí)現(xiàn):通過(guò)FutureTask Or 線程池獲取
六、線程的狀態(tài)?
新建(NEW)?:創(chuàng)建后尚未啟動(dòng)的線程的狀態(tài)
運(yùn)行(Runnable)?:包含Running和Ready
無(wú)限期等待(Waiting)?:不會(huì)被分配CPU執(zhí)行時(shí)間,需要顯式被喚醒
沒(méi)有設(shè)置Timeout參數(shù)的Object.wait()方法。
沒(méi)有設(shè)置Timeout參數(shù)的Thread.join()方法。
LockSupport.park()方法。
限期等待(Timed Waiting)?:在一定時(shí)間后會(huì)由系統(tǒng)自動(dòng)喚醒
Thread.sleep()方法。
設(shè)置了Timeout參數(shù)的Object.wait()方法。
設(shè)置了Timeout參數(shù)的Thread.join()方法。
LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。
阻塞(blocked)?:等待獲取排它鎖
結(jié)束?:已終止線程的狀態(tài),線程已經(jīng)結(jié)束執(zhí)行
七、sleep和wait
sleep是Thread類(lèi)的方法,wait是Object類(lèi)中定義的方法
sleep方法可以在任何地方使用
wait方法只能在synchronized方法或者synchronized塊中使用
最本質(zhì)的區(qū)別
Thread.sleep只會(huì)讓出CPU,不會(huì)導(dǎo)致鎖行為的改變(不會(huì)釋放鎖)
Object.wait不僅讓出CPU,還會(huì)釋放已經(jīng)占有的同步資源鎖
八、notify和notifyAll的區(qū)別
notifyAll會(huì)讓所有處于等待池的線程全部進(jìn)入鎖池去競(jìng)爭(zhēng)獲取鎖的機(jī)會(huì)
notify會(huì)隨機(jī)選取一個(gè)處于等待池中的線程進(jìn)入鎖池去競(jìng)爭(zhēng)獲取鎖的機(jī)會(huì)。
九、yield函數(shù)
當(dāng)調(diào)用Thread.yield()函數(shù)時(shí),會(huì)給線程調(diào)度器一個(gè)當(dāng)前線程愿意讓出CPU使用的暗示,但是線程調(diào)度器可能會(huì)忽略這個(gè)暗示
十、中斷函數(shù)interrupt()
已經(jīng)被拋棄的方法
通過(guò)調(diào)用stop()方法停止線程
目前使用的方法
調(diào)用interrupt(),通知線程應(yīng)該中斷了
如果線程處于被阻塞狀態(tài),那么線程將立即退出被阻塞狀態(tài),并拋出一個(gè)InterruptedException異常
如果線程處于正?;顒?dòng)狀態(tài),那么會(huì)將該線程的中斷標(biāo)志設(shè)置為true。被設(shè)置中斷標(biāo)志的線程將繼續(xù)正常運(yùn)行,不受影響
需要被調(diào)用的線程配合中斷
在正常運(yùn)行任務(wù)時(shí),經(jīng)常檢查本線程的中斷標(biāo)志位,如果被設(shè)置了中斷標(biāo)志就自行停止線程。
如果線程處于正?;顒?dòng)狀態(tài),那么會(huì)將該線程的中斷標(biāo)志設(shè)置為true。被設(shè)置中斷標(biāo)志的線程將繼續(xù)正常運(yùn)行,不受影響
十一、synchronized
線程安全問(wèn)題的主要誘因:
存在共享數(shù)據(jù)(也稱(chēng)臨界資源)
存在多條線程共同操作這些共享數(shù)據(jù)
解決問(wèn)題的根本辦法:同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再對(duì)貢獻(xiàn)數(shù)據(jù)進(jìn)行操作。
互斥鎖的特性:
互斥性?:即在同一時(shí)間只允許一個(gè)線程持有某個(gè)對(duì)象鎖,通過(guò)這種特性來(lái)實(shí)現(xiàn)多線程的協(xié)調(diào)機(jī)制,這樣同一時(shí)間只有一個(gè)線程對(duì)需要同步的代碼塊(復(fù)合操作)進(jìn)行訪問(wèn)?;コ庑砸卜Q(chēng)為操作的原子性。
可見(jiàn)性?:必須確保在鎖被釋放之前,對(duì)共享變量所做的修改,對(duì)于隨后獲得該鎖的另一個(gè)線程是可見(jiàn)的(即在獲得鎖時(shí)應(yīng)該獲得最新共享變量的值),否則另一個(gè)線程可能是在本地緩存的某個(gè)副本上繼續(xù)操作,從而引起不一致。(一致性???paxos???raft???)
根據(jù)獲取鎖的分類(lèi):獲取對(duì)象鎖和獲取類(lèi)鎖
獲取對(duì)象鎖的兩種用法
同步代碼塊(synchronized(this),synchronized(類(lèi)實(shí)例對(duì)象)),鎖是小括號(hào)()中的實(shí)例對(duì)象。
同步非靜態(tài)方法(synchronized method),鎖是當(dāng)前對(duì)象的實(shí)例對(duì)象。
獲取類(lèi)鎖的兩種用法
同步代碼塊(synchronized(類(lèi).class)),鎖是小括號(hào)()中的類(lèi)對(duì)象(Class對(duì)象)。
同步靜態(tài)方法(synchronized static method),鎖是當(dāng)前對(duì)象的類(lèi)對(duì)象(Class對(duì)象)
對(duì)象鎖和類(lèi)鎖的總結(jié):
有線程訪問(wèn)對(duì)象的同步代碼塊時(shí),另外的線程可以訪問(wèn)該對(duì)象的非同步代碼塊;
若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的同步代碼塊時(shí),另一個(gè)訪問(wèn)對(duì)象的同步代碼塊的線程會(huì)被阻塞;
若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的同步方法時(shí),另一個(gè)訪問(wèn)對(duì)象的同步方法的線程會(huì)被阻塞;
若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的同步代碼塊時(shí),另一個(gè)訪問(wèn)對(duì)象的同步方法的線程會(huì)被阻塞;,反之亦然;
同一個(gè)類(lèi)的不同對(duì)象的對(duì)象鎖互不干擾;
類(lèi)鎖由于也是一種特殊的對(duì)象鎖,因此表現(xiàn)和上述1、2、3、4一致,而由于一個(gè)類(lèi)只有一把對(duì)象鎖,所以同一個(gè)類(lèi)的不同對(duì)象使用類(lèi)鎖將會(huì)是同步的;
類(lèi)鎖和對(duì)象鎖互不干擾。
十二、synchronized的底層實(shí)現(xiàn)原理
1. 實(shí)現(xiàn)synchronized的基礎(chǔ)
java對(duì)象頭
Monitor
2. 對(duì)象在內(nèi)存中的布局
對(duì)象頭
實(shí)例數(shù)據(jù)
對(duì)齊填充
對(duì)象頭的結(jié)構(gòu):
java的對(duì)象頭由以下三部分組成:
Mark Word
指向類(lèi)的指針
數(shù)組長(zhǎng)度(只有數(shù)組對(duì)象才有)
Mark Word Mark Word記錄了對(duì)象和鎖有關(guān)的信息,當(dāng)這個(gè)對(duì)象被synchronized關(guān)鍵字當(dāng)成同步鎖時(shí),圍繞這個(gè)鎖的一系列操作都和Mark Word有關(guān)。
Mark Word在32位JVM中的長(zhǎng)度是32bit,在64位JVM中長(zhǎng)度是64bit。
Mark Word在不同的鎖狀態(tài)下存儲(chǔ)的內(nèi)容不同,在32位JVM中是這么存的:
JVM一般是這樣使用鎖和Mark Word的:
當(dāng)沒(méi)有被當(dāng)成鎖時(shí),這就是一個(gè)普通的對(duì)象,Mark Word記錄對(duì)象的HashCode,鎖標(biāo)志位是01,是否偏向鎖那一位是0。
當(dāng)對(duì)象被當(dāng)做同步鎖并有一個(gè)線程A搶到了鎖時(shí),鎖標(biāo)志位還是01,但是否偏向鎖那一位改成1,前23bit記錄搶到鎖的線程id,表示進(jìn)入偏向鎖狀態(tài)。
當(dāng)線程A再次試圖來(lái)獲得鎖時(shí),JVM發(fā)現(xiàn)同步鎖對(duì)象的標(biāo)志位是01,是否偏向鎖是1,也就是偏向狀態(tài),Mark Word中記錄的線程id就是線程A自己的id,表示線程A已經(jīng)獲得了這個(gè)偏向鎖,可以執(zhí)行同步鎖的代碼。
當(dāng)線程B試圖獲得這個(gè)鎖時(shí),JVM發(fā)現(xiàn)同步鎖處于偏向狀態(tài),但是Mark Word中的線程id記錄的不是B,那么線程B會(huì)先用CAS操作試圖獲得鎖,這里的獲得鎖操作是有可能成功的,因?yàn)榫€程A一般不會(huì)自動(dòng)釋放偏向鎖。如果搶鎖成功,就把Mark Word里的線程id改為線程B的id,代表線程B獲得了這個(gè)偏向鎖,可以執(zhí)行同步鎖代碼。如果搶鎖失敗,則繼續(xù)執(zhí)行步驟5。
偏向鎖狀態(tài)搶鎖失敗,代表當(dāng)前鎖有一定的競(jìng)爭(zhēng),偏向鎖將升級(jí)為輕量級(jí)鎖。JVM會(huì)在當(dāng)前線程的線程棧中開(kāi)辟一塊單獨(dú)的空間,里面保存指向?qū)ο箧iMark Word的指針,同時(shí)在對(duì)象鎖Mark Word中保存指向這片空間的指針。上述兩個(gè)保存操作都是CAS操作,如果保存成功,代表線程搶到了同步鎖,就把Mark Word中的鎖標(biāo)志位改成00,可以執(zhí)行同步鎖代碼。如果保存失敗,表示搶鎖失敗,競(jìng)爭(zhēng)太激烈,繼續(xù)執(zhí)行步驟6。
輕量級(jí)鎖搶鎖失敗,JVM會(huì)使用自旋鎖,自旋鎖不是一個(gè)鎖狀態(tài),只是代表不斷的重試,嘗試搶鎖。從JDK1.7開(kāi)始,自旋鎖默認(rèn)啟用,自旋次數(shù)由JVM決定。如果搶鎖成功則執(zhí)行同步鎖代碼,如果失敗則繼續(xù)執(zhí)行步驟7。
自旋鎖重試之后如果搶鎖依然失敗,同步鎖會(huì)升級(jí)至重量級(jí)鎖,鎖標(biāo)志位改為10。在這個(gè)狀態(tài)下,未搶到鎖的線程都會(huì)被阻塞。
Monitor(管程):每個(gè)java對(duì)象天生自帶了一把看不見(jiàn)的鎖
Monitor鎖的競(jìng)爭(zhēng)、獲取與釋放
十三、自旋鎖
許多情況下,共享數(shù)據(jù)的所狀態(tài)持續(xù)時(shí)間較短,切換線程不值得。
通過(guò)讓線程執(zhí)行忙循環(huán)等待鎖的釋放,不讓出cpu。
缺點(diǎn):若鎖被其他線程長(zhǎng)時(shí)間占用,會(huì)帶來(lái)許多性能上的開(kāi)銷(xiāo)·
十四、自適應(yīng)自旋鎖
自旋的次數(shù)不再固定
由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定
十五、鎖消除
JIT編譯時(shí),對(duì)運(yùn)行上下文進(jìn)行掃描,去除不可能存在競(jìng)爭(zhēng)的鎖。
十六、鎖粗化
通過(guò)擴(kuò)大鎖的范圍,避免反復(fù)的加鎖解鎖
十七、synchronized的四種狀態(tài)
無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖
鎖膨脹方向:無(wú)鎖 -> 偏向鎖 -> 輕量級(jí)鎖 -> 重量級(jí)鎖
偏向鎖:減少同一線程獲取鎖的代價(jià)
大多數(shù)情況下,鎖不存在多線程競(jìng)爭(zhēng),總是由同一線程多次獲得
核心思想:
如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入了偏向模式,此時(shí)Mark Word的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當(dāng)該線程再次請(qǐng)求鎖時(shí),無(wú)需再做任何同步操作,即獲取鎖的過(guò)程只需要檢查Mark Word的所標(biāo)記位為偏向鎖以及當(dāng)前線程ID等于Mark Word的ThreadID即可,這樣就省去了大量有關(guān)鎖申請(qǐng)的操作。
十八、輕量級(jí)鎖
輕量級(jí)鎖是由偏向鎖升級(jí)來(lái)的,偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下,當(dāng)?shù)诙€(gè)線程加入鎖爭(zhēng)用的時(shí)候,偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖。
?適用場(chǎng)景:?線程交替執(zhí)行同步塊
若存在同一時(shí)間訪問(wèn)同一鎖的情況,就會(huì)導(dǎo)致輕量級(jí)鎖膨脹為重量級(jí)鎖
十九、鎖的內(nèi)存語(yǔ)義
當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
當(dāng)線程獲取鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。
二十、ReenTrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的鎖。
publicclassLockExample{privateLocklock=newReentrantLock();publicvoidfunc(){lock.lock();try{for(inti =0; i <10; i++) {? ? ? ? ? ? ? ? System.out.print(i +" ");? ? ? ? ? ? }? ? ? ? }finally{lock.unlock();// 確保釋放鎖,從而避免發(fā)生死鎖。}? ? }}
public staticvoidmain(String[] args) {? ? LockExample lockExample =newLockExample();? ? ExecutorService executorService = Executors.newCachedThreadPool();? ? executorService.execute(() -> lockExample.func());executorService.execute(() -> lockExample.func());}
1. 鎖的實(shí)現(xiàn)
synchronized 是 JVM 實(shí)現(xiàn)的,而 ReentrantLock 是 JDK 實(shí)現(xiàn)的。
2. 性能
新版本 Java 對(duì) synchronized 進(jìn)行了很多優(yōu)化,例如自旋鎖等,synchronized 與 ReentrantLock 大致相同。
3. 等待可中斷
當(dāng)持有鎖的線程長(zhǎng)期不釋放鎖的時(shí)候,正在等待的線程可以選擇放棄等待,改為處理其他事情。
ReentrantLock 可中斷,而 synchronized 不行。
4. 公平鎖
公平鎖是指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的時(shí)間順序來(lái)依次獲得鎖。
synchronized 中的鎖是非公平的,ReentrantLock 默認(rèn)情況下也是非公平的,但是也可以是公平的。
5. 鎖綁定多個(gè)條件
一個(gè) ReentrantLock 可以同時(shí)綁定多個(gè) Condition 對(duì)象。
二十一、線程池
1. 為什么要用線程池?
線程池提供了一種限制和管理資源(包括執(zhí)行一個(gè)任務(wù))。 每個(gè)線程池還維護(hù)一些基本統(tǒng)計(jì)信息,例如已完成任務(wù)的數(shù)量。
這里借用《Java并發(fā)編程的藝術(shù)》提到的來(lái)說(shuō)一下使用線程池的好處:
降低資源消耗?。 通過(guò)重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷(xiāo)毀造成的消耗。
提高響應(yīng)速度?。 當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
提高線程的可管理性?。 線程是稀缺資源,如果無(wú)限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。
2. 實(shí)現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務(wù)的話(huà)需要實(shí)現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實(shí)現(xiàn)類(lèi)都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會(huì)返回結(jié)果但是 Callable 接口可以返回結(jié)果。
備注: 工具類(lèi)Executors可以實(shí)現(xiàn)Runnable對(duì)象和Callable對(duì)象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
3. 執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢?
execute() 方法用于提交不需要返回值的任務(wù),所以無(wú)法判斷任務(wù)是否被線程池執(zhí)行成功與否;
submit() 方法用于提交需要返回值的任務(wù)。線程池會(huì)返回一個(gè)Future類(lèi)型的對(duì)象,通過(guò)這個(gè)Future對(duì)象可以判斷任務(wù)是否執(zhí)行成功,并且可以通過(guò)future的get()方法來(lái)獲取返回值,get()方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成,而使用 get(long timeout,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回,這時(shí)候有可能任務(wù)沒(méi)有執(zhí)行完。
4. 如何創(chuàng)建線程池
《阿里巴巴Java開(kāi)發(fā)手冊(cè)》中強(qiáng)制線程池不允許使用 Executors 去創(chuàng)建,而是通過(guò) ThreadPoolExecutor 的方式,這樣的處理方式讓寫(xiě)的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
Executors 返回線程池對(duì)象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor?: 允許請(qǐng)求的隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE ,可能堆積大量的請(qǐng)求,從而導(dǎo)致OOM。
CachedThreadPool 和 ScheduledThreadPool?: 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會(huì)創(chuàng)建大量線程,從而導(dǎo)致OOM。
方式一:通過(guò)Executor 框架的工具類(lèi)Executors來(lái)實(shí)現(xiàn) 我們可以創(chuàng)建三種類(lèi)型的ThreadPoolExecutor:
FixedThreadPool?: 該方法返回一個(gè)固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當(dāng)有一個(gè)新的任務(wù)提交時(shí),線程池中若有空閑線程,則立即執(zhí)行。若沒(méi)有,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列中,待有線程空閑時(shí),便處理在任務(wù)隊(duì)列中的任務(wù)。
SingleThreadExecutor?: 方法返回一個(gè)只有一個(gè)線程的線程池。若多余一個(gè)任務(wù)被提交到該線程池,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列中,待線程空閑,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)。
CachedThreadPool?: 該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復(fù)用,則會(huì)優(yōu)先使用可復(fù)用的線程。若所有線程均在工作,又有新的任務(wù)提交,則會(huì)創(chuàng)建新的線程處理任務(wù)。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線程池進(jìn)行復(fù)用。
二十二、volatile關(guān)鍵字
在 JDK1.2 之前,Java的內(nèi)存模型實(shí)現(xiàn)總是從主存(即共享內(nèi)存)讀取變量,是不需要進(jìn)行特別的注意的。而在當(dāng)前的 Java 內(nèi)存模型下,線程可以把變量保存本地內(nèi)存比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫(xiě)。這就可能造成一個(gè)線程在主存中修改了一個(gè)變量的值,而另外一個(gè)線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致。
要解決這個(gè)問(wèn)題,就需要把變量聲明為volatile,這就指示 JVM,這個(gè)變量是不穩(wěn)定的,每次使用它都到主存中進(jìn)行讀取。
說(shuō)白了, volatile 關(guān)鍵字的主要作用就是保證變量的可見(jiàn)性然后還有一個(gè)作用是防止指令重排序。
二十三、synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
synchronized關(guān)鍵字和volatile關(guān)鍵字比較
volatile關(guān)鍵字是線程同步的輕量級(jí)實(shí)現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊。synchronized關(guān)鍵字在JavaSE1.6之后進(jìn)行了主要包括為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實(shí)際開(kāi)發(fā)中使用 synchronized 關(guān)鍵字的場(chǎng)景還是更多一些。
多線程訪問(wèn)volatile關(guān)鍵字不會(huì)發(fā)生阻塞,而synchronized關(guān)鍵字可能會(huì)發(fā)生阻塞
volatile關(guān)鍵字能保證數(shù)據(jù)的可見(jiàn)性,但不能保證數(shù)據(jù)的原子性。synchronized關(guān)鍵字兩者都能保證。
volatile關(guān)鍵字主要用于解決變量在多個(gè)線程之間的可見(jiàn)性,而 synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問(wèn)資源的同步性。
二十四、ThreadLocal
通常情況下,我們創(chuàng)建的變量是可以被任何一個(gè)線程訪問(wèn)并修改的。如果想實(shí)現(xiàn)每一個(gè)線程都有自己的專(zhuān)屬本地變量該如何解決呢? JDK中提供的ThreadLocal類(lèi)正是為了解決這樣的問(wèn)題。 ThreadLocal類(lèi)主要解決的就是讓每個(gè)線程綁定自己的值,可以將ThreadLocal類(lèi)形象的比喻成存放數(shù)據(jù)的盒子,盒子中可以存儲(chǔ)每個(gè)線程的私有數(shù)據(jù)。
如果你創(chuàng)建了一個(gè)ThreadLocal變量,那么訪問(wèn)這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的本地副本,這也是ThreadLocal變量名的由來(lái)。他們可以使用 get() 和 set() 方法來(lái)獲取默認(rèn)值或?qū)⑵渲蹈臑楫?dāng)前線程所存的副本的值,從而避免了線程安全問(wèn)題。
importjava.text.SimpleDateFormat;importjava.util.Random;publicclassThreadLocalExampleimplementsRunnable{// SimpleDateFormat 不是線程安全的,所以每個(gè)線程都要有自己獨(dú)立的副本privatestaticfinalThreadLocal formatter = ThreadLocal.withInitial(() ->newSimpleDateFormat("yyyyMMdd HHmm"));publicstaticvoidmain(String[] args)throwsInterruptedException{? ? ? ? ThreadLocalExample obj =newThreadLocalExample();for(inti=0; i<10; i++){? ? ? ? ? ? Thread t =newThread(obj,""+i);? ? ? ? ? ? Thread.sleep(newRandom().nextInt(1000));? ? ? ? ? ? t.start();? ? ? ? }? ? }@Overridepublicvoidrun(){? ? ? ? System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());try{? ? ? ? ? ? Thread.sleep(newRandom().nextInt(1000));? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? e.printStackTrace();? ? ? ? }//formatter pattern is changed here by thread, but it won't reflect to other threadsformatter.set(newSimpleDateFormat());? ? ? ? System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());? ? }}
Output:
ThreadName=0defaultFormatter = yyyyMMdd HHmmThreadName=0formatter = yy-M-d ah:mmThreadName=1defaultFormatter = yyyyMMdd HHmmThreadName=2defaultFormatter = yyyyMMdd HHmmThreadName=1formatter = yy-M-d ah:mmThreadName=3defaultFormatter = yyyyMMdd HHmmThreadName=2formatter = yy-M-d ah:mmThreadName=4defaultFormatter = yyyyMMdd HHmmThreadName=3formatter = yy-M-d ah:mmThreadName=4formatter = yy-M-d ah:mmThreadName=5defaultFormatter = yyyyMMdd HHmmThreadName=5formatter = yy-M-d ah:mmThreadName=6defaultFormatter = yyyyMMdd HHmmThreadName=6formatter = yy-M-d ah:mmThreadName=7defaultFormatter = yyyyMMdd HHmmThreadName=7formatter = yy-M-d ah:mmThreadName=8defaultFormatter = yyyyMMdd HHmmThreadName=9defaultFormatter = yyyyMMdd HHmmThreadName=8formatter = yy-M-d ah:mmThreadName=9formatter = yy-M-d ah:mm
原理:
從 Thread類(lèi)源代碼入手。
publicclassThreadimplementsRunnable{ ......//與此線程有關(guān)的ThreadLocal值。由ThreadLocal類(lèi)維護(hù)ThreadLocal.ThreadLocalMap threadLocals =null;//與此線程有關(guān)的InheritableThreadLocal值。由InheritableThreadLocal類(lèi)維護(hù)ThreadLocal.ThreadLocalMap inheritableThreadLocals =null; ......}
從上面Thread類(lèi) 源代碼可以看出Thread 類(lèi)中有一個(gè) threadLocals 和 一個(gè) inheritableThreadLocals 變量,它們都是 ThreadLocalMap 類(lèi)型的變量,我們可以把 ThreadLocalMap 理解為T(mén)hreadLocal 類(lèi)實(shí)現(xiàn)的定制化的 HashMap。默認(rèn)情況下這兩個(gè)變量都是null,只有當(dāng)前線程調(diào)用 ThreadLocal 類(lèi)的 set或get方法時(shí)才創(chuàng)建它們,實(shí)際上調(diào)用這兩個(gè)方法的時(shí)候,我們調(diào)用的是ThreadLocalMap類(lèi)對(duì)應(yīng)的 get()、set() 方法。
ThreadLocal類(lèi)的set()方法
publicvoidset(Tvalue){? ? ? ? Thread t = Thread.currentThread();? ? ? ? ThreadLocalMap map = getMap(t);if(map !=null)? ? ? ? ? ? map.set(this,value);elsecreateMap(t,value);? ? }ThreadLocalMapgetMap(Thread t){returnt.threadLocals;? ? }
通過(guò)上面這些內(nèi)容,我們足以通過(guò)猜測(cè)得出結(jié)論:最終的變量是放在了當(dāng)前線程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解為只是ThreadLocalMap的封裝,傳遞了變量值。 ThrealLocal 類(lèi)中可以通過(guò)Thread.currentThread()獲取到當(dāng)前線程對(duì)象后,直接通過(guò)getMap(Thread t)可以訪問(wèn)到該線程的ThreadLocalMap對(duì)象。
每個(gè)Thread中都具備一個(gè)ThreadLocalMap,而ThreadLocalMap可以存儲(chǔ)以ThreadLocal為key的鍵值對(duì)。 比如我們?cè)谕粋€(gè)線程中聲明了兩個(gè) ThreadLocal 對(duì)象的話(huà),會(huì)使用 Thread內(nèi)部都是使用僅有那個(gè)ThreadLocalMap 存放數(shù)據(jù)的,ThreadLocalMap的 key 就是 ThreadLocal對(duì)象,value 就是 ThreadLocal 對(duì)象調(diào)用set方法設(shè)置的值。 ThreadLocal 是 map結(jié)構(gòu)是為了讓每個(gè)線程可以關(guān)聯(lián)多個(gè) ThreadLocal變量。這也就解釋了 ThreadLocal 聲明的變量為什么在每一個(gè)線程都有自己的專(zhuān)屬本地變量。
ThreadLocalMap是ThreadLocal的靜態(tài)內(nèi)部類(lèi)。
二十五、ThreadLocal 內(nèi)存泄露問(wèn)題
ThreadLocalMap 中使用的 key 為 ThreadLocal 的弱引用,而 value 是強(qiáng)引用。所以,如果 ThreadLocal 沒(méi)有被外部強(qiáng)引用的情況下,在垃圾回收的時(shí)候會(huì) key 會(huì)被清理掉,而 value 不會(huì)被清理掉。這樣一來(lái),ThreadLocalMap 中就會(huì)出現(xiàn)key為null的Entry。假如我們不做任何措施的話(huà),value 永遠(yuǎn)無(wú)法被GC 回收,這個(gè)時(shí)候就可能會(huì)產(chǎn)生內(nèi)存泄露。ThreadLocalMap實(shí)現(xiàn)中已經(jīng)考慮了這種情況,在調(diào)用 set()、get()、remove() 方法的時(shí)候,會(huì)清理掉 key 為 null 的記錄。使用完 ThreadLocal方法后 最好手動(dòng)調(diào)用remove()方法。
staticclassEntryextendsWeakReference<ThreadLocal<?>>{/** The value associated with this ThreadLocal. */Objectvalue;Entry(ThreadLocal k,Objectv) {super(k);? ? ? ? ? ? ? ? value = v;? ? ? ? ? ? }? ? ? ? }
弱引用介紹:
如果一個(gè)對(duì)象只具有弱引用,那么就類(lèi)似于可有可無(wú)的生活用品。弱引用與軟引用的區(qū)別在于:只具有弱引用的對(duì)象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內(nèi)存區(qū)域的過(guò)程中,一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象,不管當(dāng)前內(nèi)存空間足夠與否,都會(huì)回收它的內(nèi)存。不過(guò),由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線程, 因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象。
弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對(duì)象被垃圾回收,Java虛擬機(jī)就會(huì)把這個(gè)弱引用加入到與之關(guān)聯(lián)的引用隊(duì)列中去。
二十六、java 線程方法join的簡(jiǎn)單總結(jié)
1. 作用
Thread類(lèi)中的join方法的主要作用就是同步,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。具體看代碼:
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");? ? ? ? t1.start();/**join的意思是使得放棄當(dāng)前線程的執(zhí)行,并返回對(duì)應(yīng)的線程,例如下面代碼的意思就是:
? ? ? ? 程序在main線程中調(diào)用t1線程的join方法,則main線程放棄cpu控制權(quán),并返回t1線程繼續(xù)執(zhí)行直到線程t1執(zhí)行完畢
? ? ? ? 所以結(jié)果是t1線程執(zhí)行完后,才到主線程執(zhí)行,相當(dāng)于在main線程中同步t1線程,t1執(zhí)行完了,main線程才有執(zhí)行的機(jī)會(huì)
? ? ? ? */t1.join();? ? ? ? t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面程序結(jié)果是先打印完小明線程,在打印小東線程;
上面注釋也大概說(shuō)明了join方法的作用:在A線程中調(diào)用了B線程的join()方法時(shí),表示只有當(dāng)B線程執(zhí)行完畢時(shí),A線程才能繼續(xù)執(zhí)行。注意,這里調(diào)用的join方法是沒(méi)有傳參的,join方法其實(shí)也可以傳遞一個(gè)參數(shù)給它的,具體看下面的簡(jiǎn)單例子:
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");? ? ? ? t1.start();/**join方法可以傳遞參數(shù),join(10)表示main線程會(huì)等待t1線程10毫秒,10毫秒過(guò)去后,
? ? ? ? * main線程和t1線程之間執(zhí)行順序由串行執(zhí)行變?yōu)槠胀ǖ牟⑿袌?zhí)行
? ? ? ? */t1.join(10);? ? ? ? t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面代碼結(jié)果是:程序執(zhí)行前面10毫秒內(nèi)打印的都是小明線程,10毫秒后,小明和小東程序交替打印。
所以,join方法中如果傳入?yún)?shù),則表示這樣的意思:如果A線程中掉用B線程的join(10),則表示A線程會(huì)等待B線程執(zhí)行10毫秒,10毫秒過(guò)后,A、B線程并行執(zhí)行。需要注意的是,jdk規(guī)定,join(0)的意思不是A線程等待B線程0秒,而是A線程等待B線程無(wú)限時(shí)間,直到B線程執(zhí)行完畢,即join(0)等價(jià)于join()。
2. join與start調(diào)用順序問(wèn)題
上面的討論大概知道了join的作用了,那么,如果 join在start前調(diào)用,會(huì)出現(xiàn)什么后果呢?先看下面的測(cè)試結(jié)果
publicclassJoinTest{publicstaticvoidmain(String [] args)throwsInterruptedException{? ? ? ? ThreadJoinTest t1 =newThreadJoinTest("小明");? ? ? ? ThreadJoinTest t2 =newThreadJoinTest("小東");/**join方法可以在start方法前調(diào)用時(shí),并不能起到同步的作用
? ? ? ? */t1.join();? ? ? ? t1.start();//Thread.yield();t2.start();? ? }}classThreadJoinTestextendsThread{publicThreadJoinTest(String name){super(name);? ? }@Overridepublicvoidrun(){for(inti=0;i<1000;i++){? ? ? ? ? ? System.out.println(this.getName() +":"+ i);? ? ? ? }? ? }}
上面代碼執(zhí)行結(jié)果是:小明和小東線程交替打印。
所以得到以下結(jié)論:join方法必須在線程start方法調(diào)用之后調(diào)用才有意義。這個(gè)也很容易理解:如果一個(gè)線程都沒(méi)有start,那它也就無(wú)法同步了。
3. join方法實(shí)現(xiàn)原理
有了上面的例子,我們大概知道join方法的作用了,那么,join方法實(shí)現(xiàn)的原理是什么呢?
其實(shí),join方法是通過(guò)調(diào)用線程的wait方法來(lái)達(dá)到同步的目的的。例如,A線程中調(diào)用了B線程的join方法,則相當(dāng)于A線程調(diào)用了B線程的wait方法,在調(diào)用了B線程的wait方法后,A線程就會(huì)進(jìn)入阻塞狀態(tài),具體看下面的源碼:
publicfinalsynchronizedvoidjoin(longmillis)throwsInterruptedException{longbase = System.currentTimeMillis();longnow =0;if(millis <0) {thrownewIllegalArgumentException("timeout value is negative");? ? ? ? }if(millis ==0) {while(isAlive()) {? ? ? ? ? ? ? ? wait(0);? ? ? ? ? ? }? ? ? ? }else{while(isAlive()) {longdelay = millis - now;if(delay <=0) {break;? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? wait(delay);? ? ? ? ? ? ? ? now = System.currentTimeMillis() - base;? ? ? ? ? ? }? ? ? ? }? ? }
從源碼中可以看到:join方法的原理就是調(diào)用相應(yīng)線程的wait方法進(jìn)行等待操作的,例如A線程中調(diào)用了B線程的join方法,則相當(dāng)于在A線程中調(diào)用了B線程的wait方法,當(dāng)B線程執(zhí)行完(或者到達(dá)等待時(shí)間),B線程會(huì)自動(dòng)調(diào)用自身的notifyAll方法喚醒A線程,從而達(dá)到同步的目的。
二十七、線程安全
多個(gè)線程不管以何種方式訪問(wèn)某個(gè)類(lèi),并且在主調(diào)代碼中不需要進(jìn)行同步,都能表現(xiàn)正確的行為。
線程安全有以下幾種實(shí)現(xiàn)方式:
1. 不可變
不可變(Immutable)的對(duì)象一定是線程安全的,不需要再采取任何的線程安全保障措施。只要一個(gè)不可變的對(duì)象被正確地構(gòu)建出來(lái),永遠(yuǎn)也不會(huì)看到它在多個(gè)線程之中處于不一致的狀態(tài)。多線程環(huán)境下,應(yīng)當(dāng)盡量使對(duì)象成為不可變,來(lái)滿(mǎn)足線程安全。
不可變的類(lèi)型:
final 關(guān)鍵字修飾的基本數(shù)據(jù)類(lèi)型
String
枚舉類(lèi)型
Number 部分子類(lèi),如 Long 和 Double 等數(shù)值包裝類(lèi)型,BigInteger 和 BigDecimal 等大數(shù)據(jù)類(lèi)型。但同為 Number 的原子類(lèi) AtomicInteger 和 AtomicLong 則是可變的。
對(duì)于集合類(lèi)型,可以使用 Collections.unmodifiableXXX() 方法來(lái)獲取一個(gè)不可變的集合。
publicclassImmutableExample{? ? publicstaticvoidmain(String[] args) {Map map =newHashMap<>();Map unmodifiableMap = Collections.unmodifiableMap(map);? ? ? ? unmodifiableMap.put("a",1);? ? }}Exceptioninthread"main"java.lang.UnsupportedOperationException? ? at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)? ? at ImmutableExample.main(ImmutableExample.java:9)
Collections.unmodifiableXXX() 先對(duì)原始的集合進(jìn)行拷貝,需要對(duì)集合進(jìn)行修改的方法都直接拋出異常。
publicVput(K key, Vvalue){thrownewUnsupportedOperationException();}
2. 互斥同步
synchronized 和 ReentrantLock。
3. 非阻塞同步
互斥同步最主要的問(wèn)題就是線程阻塞和喚醒所帶來(lái)的性能問(wèn)題,因此這種同步也稱(chēng)為阻塞同步。
互斥同步屬于一種悲觀的并發(fā)策略,總是認(rèn)為只要不去做正確的同步措施,那就肯定會(huì)出現(xiàn)問(wèn)題。無(wú)論共享數(shù)據(jù)是否真的會(huì)出現(xiàn)競(jìng)爭(zhēng),它都要進(jìn)行加鎖(這里討論的是概念模型,實(shí)際上虛擬機(jī)會(huì)優(yōu)化掉很大一部分不必要的加鎖)、用戶(hù)態(tài)核心態(tài)轉(zhuǎn)換、維護(hù)鎖計(jì)數(shù)器和檢查是否有被阻塞的線程需要喚醒等操作。
①. CAS
隨著硬件指令集的發(fā)展,我們可以使用基于沖突檢測(cè)的樂(lè)觀并發(fā)策略:先進(jìn)行操作,如果沒(méi)有其它線程爭(zhēng)用共享數(shù)據(jù),那操作就成功了,否則采取補(bǔ)償措施(不斷地重試,直到成功為止)。這種樂(lè)觀的并發(fā)策略的許多實(shí)現(xiàn)都不需要將線程阻塞,因此這種同步操作稱(chēng)為非阻塞同步。
樂(lè)觀鎖需要操作和沖突檢測(cè)這兩個(gè)步驟具備原子性,這里就不能再使用互斥同步來(lái)保證了,只能靠硬件來(lái)完成。硬件支持的原子性操作最典型的是:比較并交換(Compare-and-Swap,CAS)。CAS 指令需要有 3 個(gè)操作數(shù),分別是內(nèi)存地址 V、舊的預(yù)期值 A 和新值 B。當(dāng)執(zhí)行操作時(shí),只有當(dāng) V 的值等于 A,才將 V 的值更新為 B。
②. AtomicInteger
J.U.C 包里面的整數(shù)原子類(lèi) AtomicInteger 的方法調(diào)用了 Unsafe 類(lèi)的 CAS 操作。
以下代碼使用了 AtomicInteger 執(zhí)行了自增的操作。
privateAtomicInteger cnt =newAtomicInteger();publicvoidadd(){? ? cnt.incrementAndGet();}
以下代碼是 incrementAndGet() 的源碼,它調(diào)用了 Unsafe 的 getAndAddInt() 。
publicfinalint incrementAndGet() {returnunsafe.getAndAddInt(this, valueOffset,1) +1;}
以下代碼是 getAndAddInt() 源碼,var1 指示對(duì)象內(nèi)存地址,var2 指示該字段相對(duì)對(duì)象內(nèi)存地址的偏移,var4 指示操作需要加的數(shù)值,這里為 1。通過(guò) getIntVolatile(var1, var2) 得到舊的預(yù)期值,通過(guò)調(diào)用 compareAndSwapInt() 來(lái)進(jìn)行 CAS 比較,如果該字段內(nèi)存地址中的值等于 var5,那么就更新內(nèi)存地址為 var1+var2 的變量為 var5+var4。
可以看到 getAndAddInt() 在一個(gè)循環(huán)中進(jìn)行,發(fā)生沖突的做法是不斷的進(jìn)行重試。
publicfinalintgetAndAddInt(Object var1,longvar2,intvar4){intvar5;do{? ? ? ? var5 =this.getIntVolatile(var1, var2);? ? }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));returnvar5;}
③. ABA
如果一個(gè)變量初次讀取的時(shí)候是 A 值,它的值被改成了 B,后來(lái)又被改回為 A,那 CAS 操作就會(huì)誤認(rèn)為它從來(lái)沒(méi)有被改變過(guò)。
J.U.C 包提供了一個(gè)帶有標(biāo)記的原子引用類(lèi) AtomicStampedReference 來(lái)解決這個(gè)問(wèn)題,它可以通過(guò)控制變量值的版本來(lái)保證 CAS 的正確性。大部分情況下 ABA 問(wèn)題不會(huì)影響程序并發(fā)的正確性,如果需要解決 ABA 問(wèn)題,改用傳統(tǒng)的互斥同步可能會(huì)比原子類(lèi)更高效。
4. 無(wú)同步方案
要保證線程安全,并不是一定就要進(jìn)行同步。如果一個(gè)方法本來(lái)就不涉及共享數(shù)據(jù),那它自然就無(wú)須任何同步措施去保證正確性。
①. 棧封閉
多個(gè)線程訪問(wèn)同一個(gè)方法的局部變量時(shí),不會(huì)出現(xiàn)線程安全問(wèn)題,因?yàn)榫植孔兞看鎯?chǔ)在虛擬機(jī)棧中,屬于線程私有的。
publicclassStackClosedExample{publicvoidadd100() {? ? ? ? int cnt =0;for(int i =0; i <100; i++) {? ? ? ? ? ? cnt++;? ? ? ? }? ? ? ? System.out.println(cnt);? ? }}public staticvoidmain(String[] args) {? ? StackClosedExample example =newStackClosedExample();? ? ExecutorService executorService = Executors.newCachedThreadPool();? ? executorService.execute(() -> example.add100());executorService.execute(() -> example.add100());executorService.shutdown();}
②. 線程本地存儲(chǔ)(Thread Local Storage)
如果一段代碼中所需要的數(shù)據(jù)必須與其他代碼共享,那就看看這些共享數(shù)據(jù)的代碼是否能保證在同一個(gè)線程中執(zhí)行。如果能保證,我們就可以把共享數(shù)據(jù)的可見(jiàn)范圍限制在同一個(gè)線程之內(nèi),這樣,無(wú)須同步也能保證線程之間不出現(xiàn)數(shù)據(jù)爭(zhēng)用的問(wèn)題。
符合這種特點(diǎn)的應(yīng)用并不少見(jiàn),大部分使用消費(fèi)隊(duì)列的架構(gòu)模式(如“生產(chǎn)者-消費(fèi)者”模式)都會(huì)將產(chǎn)品的消費(fèi)過(guò)程盡量在一個(gè)線程中消費(fèi)完。其中最重要的一個(gè)應(yīng)用實(shí)例就是經(jīng)典 Web 交互模型中的“一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)服務(wù)器線程”(Thread-per-Request)的處理方式,這種處理方式的廣泛應(yīng)用使得很多 Web 服務(wù)端應(yīng)用都可以使用線程本地存儲(chǔ)來(lái)解決線程安全問(wèn)題。
可以使用 java.lang.ThreadLocal 類(lèi)來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)功能。
對(duì)于以下代碼,thread1 中設(shè)置 threadLocal 為 1,而 thread2 設(shè)置 threadLocal 為 2。過(guò)了一段時(shí)間之后,thread1 讀取 threadLocal 依然是 1,不受 thread2 的影響。
publicclassThreadLocalExample{public staticvoidmain(String[] args) {? ? ? ? ThreadLocal threadLocal =newThreadLocal();? ? ? ? Thread thread1 =newThread(() -> {? ? ? ? ? ? threadLocal.set(1);try{? ? ? ? ? ? ? ? Thread.sleep(1000);? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? ? ? System.out.println(threadLocal.get());? ? ? ? ? ? threadLocal.remove();? ? ? ? });Threadthread2=newThread(() -> {? ? ? ? ? ? threadLocal.set(2);? ? ? ? ? ? threadLocal.remove();? ? ? ? });thread1.start();thread2.start();? ? }}
為了理解 ThreadLocal,先看以下代碼:
publicclassThreadLocalExample1{public staticvoidmain(String[] args) {? ? ? ? ThreadLocal threadLocal1 =newThreadLocal();? ? ? ? ThreadLocal threadLocal2 =newThreadLocal();? ? ? ? Thread thread1 =newThread(() -> {? ? ? ? ? ? threadLocal1.set(1);? ? ? ? ? ? threadLocal2.set(1);? ? ? ? });Threadthread2=newThread(() -> {? ? ? ? ? ? threadLocal1.set(2);? ? ? ? ? ? threadLocal2.set(2);? ? ? ? });thread1.start();thread2.start();? ? }}
每個(gè) Thread 都有一個(gè) ThreadLocal.ThreadLocalMap 對(duì)象。
/* ThreadLocal values pertainingtothis thread. This mapismaintained * by the ThreadLocalclass. */ThreadLocal.ThreadLocalMap threadLocals =null;
當(dāng)調(diào)用一個(gè) ThreadLocal 的 set(T value) 方法時(shí),先得到當(dāng)前線程的 ThreadLocalMap 對(duì)象,然后將 ThreadLocal->value 鍵值對(duì)插入到該 Map 中。
publicvoidset(Tvalue){? ? Thread t = Thread.currentThread();? ? ThreadLocalMap map = getMap(t);if(map !=null)? ? ? ? map.set(this,value);elsecreateMap(t,value);}
get() 方法類(lèi)似。
publicTget() {? ? 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;returnresult;? ? ? ? }? ? }returnsetInitialValue();}
ThreadLocal 從理論上講并不是用來(lái)解決多線程并發(fā)問(wèn)題的,因?yàn)楦静淮嬖诙嗑€程競(jìng)爭(zhēng)。
在一些場(chǎng)景 (尤其是使用線程池) 下,由于 ThreadLocal.ThreadLocalMap 的底層數(shù)據(jù)結(jié)構(gòu)導(dǎo)致 ThreadLocal 有內(nèi)存泄漏的情況,應(yīng)該盡可能在每次使用 ThreadLocal 后手動(dòng)調(diào)用 remove(),以避免出現(xiàn) ThreadLocal 經(jīng)典的內(nèi)存泄漏甚至是造成自身業(yè)務(wù)混亂的風(fēng)險(xiǎn)。
③. 可重入代碼(Reentrant Code)
這種代碼也叫做純代碼(Pure Code),可以在代碼執(zhí)行的任何時(shí)刻中斷它,轉(zhuǎn)而去執(zhí)行另外一段代碼(包括遞歸調(diào)用它本身),而在控制權(quán)返回后,原來(lái)的程序不會(huì)出現(xiàn)任何錯(cuò)誤。
可重入代碼有一些共同的特征,例如不依賴(lài)于存儲(chǔ)在堆上的數(shù)據(jù)和公用的系統(tǒng)資源、用到的狀態(tài)量都由參數(shù)中傳入、不調(diào)用非可重入的方法等。
二十八、多線程開(kāi)發(fā)良好的實(shí)踐
給線程起個(gè)有意義的名字,這樣可以方便找 Bug。
縮小同步范圍,從而減少鎖爭(zhēng)用。例如對(duì)于 synchronized,應(yīng)該盡量使用同步塊而不是同步方法。
多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 這些同步類(lèi)簡(jiǎn)化了編碼操作,而用 wait() 和 notify() 很難實(shí)現(xiàn)復(fù)雜控制流;其次,這些同步類(lèi)是由最好的企業(yè)編寫(xiě)和維護(hù),在后續(xù)的 JDK 中還會(huì)不斷優(yōu)化和完善。
使用 BlockingQueue 實(shí)現(xiàn)生產(chǎn)者消費(fèi)者問(wèn)題。
多用并發(fā)集合少用同步集合,例如應(yīng)該使用 ConcurrentHashMap 而不是 Hashtable。
使用本地變量和不可變類(lèi)來(lái)保證線程安全。
使用線程池而不是直接創(chuàng)建線程,這是因?yàn)閯?chuàng)建線程代價(jià)很高,線程池可以有效地利用有限的線程來(lái)啟動(dòng)任務(wù)。