純干貨!二十八道BATJ大廠Java崗之"多線程與并發(fā)"面試題分享

一、進(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,241評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,939評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,697評(píng)論 6 409
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,182評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,406評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,772評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,973評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,638評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,866評(píng)論 1 285
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,644評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,953評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容