一舔示、編譯時(shí)與運(yùn)行時(shí)
二片效、java幾種鎖的區(qū)別
問題簡(jiǎn)答
這里我們就需要詳細(xì)解析一下AQS與公平鎖累奈、非公平鎖的概念了
公平鎖與非公平鎖
公平鎖:
多個(gè)線程按照申請(qǐng)鎖的順序去獲得鎖怯邪。線程會(huì)直接進(jìn)入到隊(duì)列去排隊(duì)次屠,永遠(yuǎn)都是隊(duì)列第一位才能得到鎖馏予。
優(yōu)點(diǎn):所有的線程都能得到資源掀宋,不會(huì)餓死在隊(duì)列中深纲。
缺點(diǎn):吞吐量會(huì)下降很多。隊(duì)列里面除了第一個(gè)線程布朦,其它的線程都會(huì)阻塞囤萤,cpu喚醒阻塞線程的開銷會(huì)很大。
簡(jiǎn)單來說是趴,缺點(diǎn)比優(yōu)點(diǎn)大涛舍,所以ReentrantLock默認(rèn)實(shí)現(xiàn)是非公平鎖。
非公平鎖:
多個(gè)線程去獲取所的時(shí)候唆途,會(huì)直接嘗試獲取富雅。獲取不到再進(jìn)入等待隊(duì)列掸驱,如果能獲取到就直接獲取鎖。
優(yōu)點(diǎn):可以減少CPU喚醒線程的開銷没佑,整體的吞吐率會(huì)比非公平鎖高毕贼。CPU也不用喚醒所有線程,會(huì)減少喚起線程的數(shù)量蛤奢。
缺點(diǎn):可能導(dǎo)致隊(duì)列中間的線程一直獲取不到所或者長(zhǎng)時(shí)間獲取不到鎖導(dǎo)致餓死鬼癣。
我們從ReentrantLock入手:
當(dāng)我們new一個(gè)ReentrantLock的時(shí)候,可以看到:
可以看到在ReentrantLock的構(gòu)造方方法里會(huì)創(chuàng)建Sync對(duì)象啤贩,fair默認(rèn)為false待秃,sync實(shí)現(xiàn)為NonfairSync(),fair為true的時(shí)候痹屹,sync實(shí)現(xiàn)為FairSync()章郁。
NonfairSync與FairSync都繼承自Sync對(duì)象。
Sync是ReentrantLock中的一個(gè)抽象靜態(tài)內(nèi)部類志衍,其繼承自
AbstractQueueSynchronizer
.這個(gè)AbstractQueueSynchronizer實(shí)際上就是我們常說的AQS我們先看一下NonfairSync和FairSync里公平鎖與非公平鎖如何實(shí)現(xiàn)的:
①NonfairSync
可以看到NonfairSync中并不會(huì)判斷是否有阻塞隊(duì)列的存在暖庄,而是直接調(diào)用UnSafe里的compareAndSwap(native層實(shí)現(xiàn))也就是CAS去獲取鎖。
②FairSync
而FairSync會(huì)判斷當(dāng)前線程是否位于同步隊(duì)列的首位楼肪,是返回true培廓,如果通過CAS也能獲取到鎖,則當(dāng)前線程拿到當(dāng)前鎖
公平鎖與非公平鎖 中鎖的獲取過程
我們?cè)偻锟纯垂芥i和非公平鎖是如何給當(dāng)前線程加鎖的淹辞,先看一下非公平鎖的加鎖過程:
非公平鎖獲取鎖過程
A線程準(zhǔn)備獲取鎖医舆,首先判斷一下state狀態(tài),如果是0象缀,CAS成功蔬将。將自己修改為持有鎖的那個(gè)線程。
這個(gè)時(shí)候B線程也過來了央星,判斷一下state狀態(tài)霞怀,發(fā)現(xiàn)是1,那么CAS就失敗了莉给,只能去等待隊(duì)列里等待喚醒毙石。
A持有鎖結(jié)束,準(zhǔn)備釋放掉鎖颓遏。會(huì)修改state狀態(tài)徐矩,抹掉持有鎖線程的痕跡,準(zhǔn)備去叫醒B。
也就是A線程調(diào)用了unLock方法:
這時(shí)候線程C進(jìn)來了,發(fā)現(xiàn)state是0二打,果斷CAS后修改state為1扛伍,將持有鎖的線程修改為自己鳞骤。
B線程被A喚醒準(zhǔn)備去獲取鎖窒百,結(jié)果發(fā)現(xiàn)state是1,CAS失敗豫尽,直接繼續(xù)去等待隊(duì)列篙梢。
以上就是一個(gè)非公平鎖中線程獲取鎖的過程。
公平鎖獲取鎖過程
ReentrantLock構(gòu)造參數(shù)中傳true美旧,改成公平鎖渤滞,默認(rèn)非公平鎖。
①線程A想要獲取鎖陈症,先判斷一下state蔼水,發(fā)現(xiàn)是0震糖;看了一下隊(duì)列录肯,自己是第一位,果斷將鎖持有線程改為自己吊说。
②線程B過來了论咏,判斷一下state,是1颁井,CAS失敗厅贪,只能去排隊(duì)。
③線程A釋放鎖之后雅宾,喚醒B养涮,這時(shí)候線程C進(jìn)來了,先判斷一下state是0眉抬,以為有戲贯吓,但是發(fā)現(xiàn)自己不是等待隊(duì)列中的第一位,作為良好市民蜀变,果斷去排隊(duì)了悄谐。
④線程B被A歡喜你后,去判斷state库北,發(fā)現(xiàn)是0爬舰,且自己現(xiàn)在是隊(duì)列中的第一位,那么獲得了當(dāng)前鎖寒瓦。
//TODO 需要將源碼串聯(lián)起來情屹,根據(jù)代碼梳理這些流程。
AQS
AQS詳解
AQS詳解二
AQS定義了一套多線程訪問共享資源的同步器框架杂腰,許多同步類實(shí)現(xiàn)都依賴于它垃你,如常用的ReentrantLock/Semaphore/CountDownLatch...。
AQS為一系列同步器依賴于一個(gè)單獨(dú)的原子變量(state)的同步器提供了一個(gè)非常有用的基礎(chǔ)。子類們必須定義改變state變量的protected方法蜡镶,這些方法定義了state是如何被獲取或釋放的雾袱。鑒于此,本類中的其他方法執(zhí)行所有的排隊(duì)和阻塞機(jī)制官还。子類也可以維護(hù)其他的state變量芹橡,但是為了保證同步,必須原子地操作這些變量望伦。
AbstractQueuedSynchronizer中對(duì)state的操作是原子的林说,且不能被繼承。所有的同步機(jī)制的實(shí)現(xiàn)均依賴于對(duì)改變量的原子操作屯伞。為了實(shí)現(xiàn)不同的同步機(jī)制腿箩,我們需要?jiǎng)?chuàng)建一個(gè)非共有的(non-public internal)擴(kuò)展了AQS類的內(nèi)部輔助類來實(shí)現(xiàn)相應(yīng)的同步邏輯。
AQS為一系列同步器依賴于一個(gè)單獨(dú)的原子變量(state)的同步器提供了一個(gè)非常有用的基礎(chǔ)劣摇。子類們必須定義改變state變量的protected方法珠移,這些方法定義了state是如何被獲取或釋放的。鑒于此末融,本類中的其他方法執(zhí)行所有的排隊(duì)和阻塞機(jī)制钧惧。子類也可以維護(hù)其他的state變量,但是為了保證同步勾习,必須原子地操作這些變量浓瞪。
三、線程池相關(guān)
Java線程池入門
線程池面試問題總結(jié)--很nice巧婶,必須看看
美團(tuán)技術(shù)博客--線程池
系統(tǒng)中有四個(gè)自帶的線程池乾颁,java建議使用這個(gè)幾個(gè)來建議線程池,但是阿里巴巴禁止用四個(gè)Executors創(chuàng)建的線程池R照弧(為什么禁止我們后面說英岭,先簡(jiǎn)單說一下這個(gè)四個(gè)線程池)
1.newSingleThreadExecutor
創(chuàng)建一個(gè)單線程的線程池。這個(gè)線程池只有一個(gè)線程在工作眼滤,也就是相當(dāng)于單線程串行執(zhí)行所有任務(wù)巴席。如果這個(gè)唯一的線程因?yàn)楫惓=Y(jié)束,那么會(huì)有一個(gè)新的線程來替代它诅需。此線程池保證所有任務(wù)的執(zhí)行順序按照任務(wù)的提交順序執(zhí)行漾唉。
2.newFixedThreadPool
創(chuàng)建固定大小的線程池。每次提交一個(gè)任務(wù)就創(chuàng)建一個(gè)線程堰塌,直到線程達(dá)到線程的最大容量赵刑。線程池的大小一旦達(dá)到最大值就會(huì)保持不變,如果某個(gè)線程因?yàn)閳?zhí)行異常而結(jié)束场刑,那么線程池會(huì)補(bǔ)充一個(gè)新線程般此。
3.newCachedThreadPool
創(chuàng)建一個(gè)可緩存的線程池。如果線程池的大小超過了處理任務(wù)所需要的線程,那么就會(huì)回收部分空閑(60s不執(zhí)行任務(wù))的線程铐懊,當(dāng)任務(wù)數(shù)增加時(shí)邀桑,此線程池又可以智能的增加新線程來處理任務(wù)。此線程池不會(huì)對(duì)線程池大小做限制科乎,線程池大小完全依賴操作系統(tǒng)(或者說JVM壁畸,哈哈一個(gè)字不差,全是照著敲的)能夠創(chuàng)建的最大線程大小
4.newScheduledThreadPool
創(chuàng)建一個(gè)大小無限的線程池茅茂。此線程池支持定時(shí)以及周期性執(zhí)行任務(wù)的需求捏萍。(定時(shí)任務(wù),類似Timer和TimerTask)
前面說了阿里強(qiáng)制線程池不允許使用Executors去創(chuàng)建空闲,而是通過ThreadPoolExecutor的方式令杈,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)碴倾。
說明:Executors返回的線程池對(duì)象的弊端如下:
①FixedThreadPool和SingleThreadPool(newSingleThreadExecutor):
允許的請(qǐng)求隊(duì)列長(zhǎng)度為Integer.MAX_VALUE,可能堆積大量的請(qǐng)求逗噩,從而導(dǎo)致OOM.
②CachedThreadPool和ScheduledThreadPool:
運(yùn)行創(chuàng)建的線程數(shù)量為Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致OOM影斑。
Demo:
創(chuàng)建一個(gè)死循環(huán)塞進(jìn)線程池線程给赞,最終會(huì)報(bào)OOM異常。
Java中的 BlockingQueue主要有兩種實(shí)現(xiàn)矫户,分別是ArrayBlockingQueue 和 LinkedBlockingQueue。
ArrayBlockingQueue是一個(gè)用數(shù)組實(shí)現(xiàn)的有界阻塞隊(duì)列残邀,必須設(shè)置容量皆辽。
LinkedBlockingQueue是一個(gè)用鏈表實(shí)現(xiàn)的有界阻塞隊(duì)列,容量可以選擇進(jìn)行設(shè)置芥挣,不設(shè)置的話驱闷,將是一個(gè)無邊界的阻塞隊(duì)列,最大長(zhǎng)度為Integer.MAX_VALUE空免。
這里的問題就出在:不設(shè)置的話空另,將是一個(gè)無邊界的阻塞隊(duì)列,最大長(zhǎng)度為Integer.MAX_VALUE蹋砚。也就是說扼菠,如果我們不設(shè)置LinkedBlockingQueue的容量的話,其默認(rèn)容量將會(huì)是Integer.MAX_VALUE坝咐。
而newFixedThreadPool中創(chuàng)建LinkedBlockingQueue時(shí)循榆,并未指定容量。此時(shí)墨坚,LinkedBlockingQueue就是一個(gè)無邊界隊(duì)列秧饮,對(duì)于一個(gè)無邊界隊(duì)列來說,是可以不斷的向隊(duì)列中加入任務(wù)的,這種情況下就有可能因?yàn)槿蝿?wù)過多而導(dǎo)致內(nèi)存溢出問題盗尸。
拋出異常的點(diǎn)為L(zhǎng)inkedBlockingQueue.offer方法.我們先看一下這四個(gè)線程池都是怎么創(chuàng)建的:
可以看到這四個(gè)線程池底層都是ThreadPoolExecutors創(chuàng)建的柑船。ThreadPoolExecutors構(gòu)造參數(shù),有七個(gè)泼各。如下:
①corePoolSize:線程池中核心線程的數(shù)量(看源碼注釋:一直在線程池中的線程個(gè)數(shù))
②maximumPoolSize:線程池允許的最大的線程數(shù)
③keepAliveTime:非核心空閑線程最大存活時(shí)間椎组,一個(gè)線程如果處于空閑狀態(tài),且當(dāng)前線程超過核心線程數(shù)历恐,那么在這個(gè)指定時(shí)間后寸癌,線程會(huì)被銷毀。
④TimeUnit:時(shí)間單位
**⑤workQueue:也就是我們常說的阻塞隊(duì)列:BlockingQueue弱贼。jdk提供了四種工作隊(duì)列:
1>ArrayBlockingQueue
基于數(shù)組的有界阻塞隊(duì)列蒸苇,按FIFO排序。新任務(wù)進(jìn)來后吮旅,會(huì)放到該隊(duì)列的隊(duì)尾溪烤,有界的數(shù)組可以防止資源耗盡問題。當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize后庇勃,再有新任務(wù)進(jìn)來檬嘀,則會(huì)將任務(wù)放入該隊(duì)列的隊(duì)尾,等待被調(diào)度责嚷。如果隊(duì)列已經(jīng)滿了鸳兽,則創(chuàng)建一個(gè)新線程,如果線程數(shù)量已經(jīng)達(dá)到maxPoolSize罕拂,則會(huì)執(zhí)行拒絕策略揍异。
2>LinkedBlockingQueue
基于鏈表的無界阻塞隊(duì)列。(最大容量為Integer.MAX_VALUE)爆班,按照FIFO排序衷掷。由于該隊(duì)列的近似無界性服球,當(dāng)線程池中線程數(shù)量達(dá)到corePoolSize后趁猴,再有新任務(wù)盡力啊逢渔,會(huì)一直存在該隊(duì)列予借,而不會(huì)去創(chuàng)建新線程直到maxPoolSize(Integer.MAX_VALUE)因此使用該工作隊(duì)列時(shí)雕沉,參數(shù)maxPoolSize其實(shí)是不起作用的(而SingleThreadPool和FixedThreadPool默認(rèn)用的就是LinkedBlockingQueue捐韩,所以可能會(huì)導(dǎo)致OOM!!)瘟仿。
3>SynchronousQueue
一個(gè)不緩存任務(wù)的阻塞隊(duì)列梦碗,生產(chǎn)者放入一個(gè)任務(wù)必須等到消費(fèi)者取出這個(gè)任務(wù)祟辟。也就是說新任務(wù)進(jìn)來時(shí)医瘫,不會(huì)緩存,而是直接被調(diào)度執(zhí)行該任務(wù)旧困。如果沒有可用線程醇份,則創(chuàng)建新線程稼锅,如果線程數(shù)量達(dá)到maxPoolSize,則執(zhí)行拒絕策略.
4>PriorityBlockingQueue
具有優(yōu)先級(jí)的無界阻塞隊(duì)列,優(yōu)先級(jí)通過參數(shù)Comparator實(shí)現(xiàn)僚纷。(優(yōu)先級(jí)!!這個(gè)好使>鼐唷!)
**
⑥threadFactory線程工廠
創(chuàng)建一個(gè)新線程時(shí)使用的工廠怖竭,可以用來設(shè)定線程名锥债、是否為daemon線程等。
⑦h(yuǎn)andler拒絕策略
當(dāng)工作隊(duì)列中的任務(wù)已達(dá)到最大限制痊臭,并且線程池中的線程數(shù)量也達(dá)到最大限制哮肚。這時(shí)如果有新任務(wù)提交進(jìn)來時(shí),該如何處理呢广匙。這里的拒絕策略允趟,這時(shí)解決這個(gè)問題的,jdk中提供了4種拒絕策略:
1>CallerRunsPolicy
該策略下鸦致,在調(diào)用者線程中執(zhí)行執(zhí)行被拒絕任務(wù)的run方法潮剪,除非線程池已經(jīng)shutdown,則直接拋棄任務(wù)分唾。
2>AbortPolicy
該策略下抗碰,直接丟棄任務(wù),并拋出RejectedExecutioinException異常绽乔。
3>DiscardPolicy
該策略下弧蝇,直接丟棄任務(wù),什么都不做迄汛。
4>DiscardOldestPolicy
該策略下捍壤,拋棄進(jìn)入隊(duì)列最早的那個(gè)任務(wù),然后嘗試把這次拒絕的任務(wù)放入隊(duì)列鞍爱。
創(chuàng)建線程池的正確姿勢(shì)
避免使用Executors創(chuàng)建線程池,主要是避免使用其中的默認(rèn)實(shí)現(xiàn)专酗,那么我們可以自己直接調(diào)用ThreadPoolExecutor的構(gòu)造參數(shù)來自己創(chuàng)建線程池睹逃,給BlockingQueu指定容量就可以了。比如我們創(chuàng)建一個(gè)核心線程池祷肯,也就是newFixedThreadPool:
這種情況下沉填,一旦提交的線程數(shù)超過當(dāng)前可用線程數(shù)時(shí),就會(huì)拋出java.untl.concurrent.RejectdExecutionException.異常(Exeception)總比錯(cuò)誤(Error)好佑笋。
說到錯(cuò)誤和異常翼闹,我們下面簡(jiǎn)單說一下程序中捕獲異常和錯(cuò)誤的問題。
java exception和 error
java 中error可不可以捕獲蒋纬?答案是可以的猎荠。
可以看到error和exception都繼承自Throwable坚弱,我們完全可以在catch語句中捕獲Throwable,也就是error可以捕獲:
那為什么不該捕獲Error呢关摇?因?yàn)槌霈F(xiàn)Error的情況會(huì)造成程序直接無法運(yùn)行荒叶,所以捕獲了也沒有任何意義。
Throwable 是所有異常和錯(cuò)誤的超類输虱。你可以在 catch 子句中使用它些楣,但是你永遠(yuǎn)不應(yīng)該這樣做!
如果在 catch 子句中使用 Throwable 宪睹,它不僅會(huì)捕獲所有異常愁茁,也將捕獲所有的錯(cuò)誤。JVM 拋出錯(cuò)誤亭病,指出不應(yīng)該由應(yīng)用程序處理的嚴(yán)重問題鹅很。 典型的例子是 OutOfMemoryError 或者 StackOverflowError 。兩者都是由應(yīng)用程序控制之外的情況引起的命贴,無法處理道宅。
所以,最好不要捕獲 Throwable 胸蛛,除非你確定自己處于一種特殊的情況下能夠處理錯(cuò)誤
Runnable/Callable/Future/FutureTask
①Runnable是一個(gè)接口污茵,只有一個(gè)run方法,沒有返回值葬项,不能拋出異常泞当。
②Callable
參考資料
Callable也是一個(gè)接口,只有一個(gè)call方法民珍。和Runnable差別在于它有返回的結(jié)果襟士,而且可以拋出異常!一般配合ThreadPoolExecutor使用嚷量。[??這是真的嗎]
Callable確實(shí)主要在線程池中使用,如下圖,在ThreadPoolExecutor的父類AbstractExecutorService中有引用到Callable:
③Future也是一個(gè)接口陋桂,它可以對(duì)具體的Runnable或者Callable任務(wù)進(jìn)行取消、判斷任務(wù)是否已取消蝶溶、查詢?nèi)蝿?wù)是否完成嗜历、獲取任務(wù)結(jié)果。如果Runnable的話返回的結(jié)果是null抖所。(下面會(huì)剖析為什么Runnable的任務(wù)梨州,F(xiàn)uture還能返回結(jié)果)。接口里面有一以下幾個(gè)方法田轧。注意兩個(gè)get方法都會(huì)阻塞當(dāng)前調(diào)用get的線程暴匠,直到返回結(jié)果或者超時(shí)才會(huì)喚醒當(dāng)前的線程。
使用demo:
④FutureTask
因?yàn)镕uture只是一個(gè)接口傻粘,所以無法創(chuàng)建使用每窖,因此有了FutureTask.
FutureTask相當(dāng)于繼承了Runnable和Future帮掉。
因此它可以作為Runnable被線程執(zhí)行,又可以有Future的那些操作岛请。
Demo:
as we all know旭寿,線程池執(zhí)行任務(wù)有兩種方法,一是execute崇败,而是submit盅称。
如果們需要返回任務(wù)的執(zhí)行結(jié)果就得調(diào)用submit方法而不是execute。
submit也不神秘后室,就是將任務(wù)封裝成FutureTask再execute缩膝。
所以submit三個(gè)方法其實(shí)都是把task轉(zhuǎn)成FutureTask,如果task是Callable岸霹,就直接賦值疾层。如果是Runnable就轉(zhuǎn)為Callable再賦值,只不過返回值是null贡避。
此外痛黎,sumbit有個(gè)方法:
demo:
傳入runnable以及result,可以拿到修改后的result刮吧。
簡(jiǎn)單總結(jié):
①Callable可以獲得任務(wù)結(jié)果和拋出異常湖饱;②Runnable沒結(jié)果也無法拋出異常。
③Future可以很容易的獲取異步執(zhí)行的結(jié)果杀捻,并且對(duì)任務(wù)進(jìn)行一些操控井厌。并且get等待結(jié)果時(shí)會(huì)阻塞,所以當(dāng)任務(wù)之間有依賴關(guān)系的時(shí)候致讥,一個(gè)任務(wù)依賴另一個(gè)任務(wù)的結(jié)果仅仆,可以用Future的get來等待依賴的任務(wù)完成的結(jié)果。④FutureTask就是具體的實(shí)現(xiàn)類垢袱,有Runnable的特性又有Future的特性墓拜,內(nèi)部包的是Callable,當(dāng)然也有接受Runnable的構(gòu)造器请契,只是會(huì)偷偷把Runnable轉(zhuǎn)成Callable來實(shí)現(xiàn)能返回結(jié)果的方法撮弧。
線程池方法源碼解析
當(dāng)線程池調(diào)用該方法時(shí),線程池的狀態(tài)通過CAS會(huì)變成SHUTDOWN狀態(tài)姚糊。此時(shí),不能再往線程池中添加任務(wù)授舟,否則會(huì)拋出RejectedExecutionException救恨。
有幾個(gè)相關(guān)問題由此產(chǎn)生:
1>shutdown()有什么功能?
阻止新來的任務(wù)提交释树,對(duì)已經(jīng)提交了的任務(wù)不會(huì)產(chǎn)生任何影響肠槽。會(huì)將那些閑置的線程idleWorks進(jìn)行中斷擎淤。
2>如何組織新來的任務(wù)提交?
通過線程池的狀態(tài)改成SHUTDOWN秸仙,當(dāng)再執(zhí)行execute提交任務(wù)時(shí)嘴拢,如果測(cè)試到狀態(tài)不為RUNNING,則拋出RejectedExecutionException。如下圖:
3>為什么對(duì)運(yùn)行中的任務(wù)不產(chǎn)生任何影響寂纪?
在調(diào)用中斷任務(wù)的方法時(shí)席吴,tryTerminate方法會(huì)檢測(cè)workers中的線程,如果沒有中斷捞蛋,并且是空閑線程孝冒,才會(huì)去中斷這個(gè)線程。
這里判斷空閑線程的方法也很簡(jiǎn)單拟杉,就是看是否從ReentrantLock中獲取到鎖庄涡,如果獲取到了,說明不是在運(yùn)行中的線程搬设,為什么呢穴店?
因?yàn)榫€程在運(yùn)行之前會(huì)調(diào)用w.tryLock,會(huì)先拿到鎖執(zhí)行任務(wù);那么這個(gè)中斷的地方tryLock就會(huì)失敗拿穴,也就拿不到鎖适秩,自然也就中斷不了運(yùn)行中的線程了。
這也就是shutdown為什么關(guān)閉不了運(yùn)行中的任務(wù)的原因戏锹。
②shutDownNow
從最上層的調(diào)用可以看到糕篇,與shutdown不同的是通過CAS將狀態(tài)改成STOP,shutdown是改成了SHUTDOWN.在組織新來的任務(wù)提交的同時(shí),會(huì)中斷當(dāng)前正在運(yùn)行的線程该窗,及workes中的線程弟蚀。另外將workQueue中的任務(wù)給移除,并將這些任務(wù)添加到列表中酗失。
可以看到在interruptWorkers方法中义钉,會(huì)加鎖判斷所有的Worker是否已經(jīng)運(yùn)行且沒有被中斷,如果滿足上述條件规肴,則調(diào)用interrupt方法中斷所有運(yùn)行的線程捶闸。
接著調(diào)用drainQueue,將阻塞隊(duì)列也就是workQueue(BlockingQueue)中的任務(wù)給移除掉拖刃,并將這些任務(wù)添加到列表中返回删壮。
然后調(diào)用tryTerminate,將空閑線程也給中斷掉兑牡,CAS判斷后終止線程池央碟。
相關(guān)問題:
①如何阻止新來的任務(wù)提交?
通過將線程池的狀態(tài)改成STOP均函,當(dāng)再將執(zhí)行execute提交任務(wù)時(shí)亿虽,如果測(cè)試到狀態(tài)不為RUNNING菱涤,則拋出rejectedExecution,從而達(dá)到阻止新任務(wù)提交的目的洛勉。
②如果我提交的任務(wù)代碼塊中粘秆,正在等待某個(gè)資源,而這個(gè)資源沒到收毫,但此時(shí)執(zhí)行shutdownNow()攻走,會(huì)出現(xiàn)什么情況?
當(dāng)執(zhí)行shutdownNow()方法時(shí)牛哺,如遇已經(jīng)激活的任務(wù)陋气,并且處于阻塞狀態(tài)時(shí),shutdownNow()會(huì)執(zhí)行1次中斷阻塞的操作引润,此時(shí)對(duì)應(yīng)的線程報(bào)InterruptedException巩趁,如果后續(xù)還要等待某個(gè)資源,則按正常邏輯等待某個(gè)資源的到達(dá)淳附。例如议慰,一個(gè)線程正在sleep狀態(tài)中,此時(shí)執(zhí)行shutdownNow()奴曙,它向該線程發(fā)起interrupt()請(qǐng)求别凹,而sleep()方法遇到有interrupt()請(qǐng)求時(shí),會(huì)拋出InterruptedException()洽糟,并繼續(xù)往下執(zhí)行炉菲。在這里要提醒注意的是,在激活的任務(wù)中坤溃,如果有多個(gè)sleep(),該方法只會(huì)中斷第一個(gè)sleep()拍霜,而后面的仍然按照正常的執(zhí)行邏輯進(jìn)行。
下面看看線程池的一些問題:
(1)線程池是什么薪介,有什么好處祠饺?
線程池是一種基于池化思想管理線程的工具。 好處如下:
①降低資源消耗:通過池化技術(shù)重復(fù)利用已創(chuàng)建的線程汁政,降低線程創(chuàng)建和銷毀造成損耗道偷。
②提高響應(yīng)速度:任務(wù)到達(dá)時(shí),無需等待線程創(chuàng)建即可立即執(zhí)行
③提高線程的可管理性:線程是稀缺資源记劈,如果無限制創(chuàng)建勺鸦,不僅會(huì)消耗系統(tǒng)資源,還會(huì)因線程的不合理分布導(dǎo)致資源調(diào)度失衡目木,降低系統(tǒng)的穩(wěn)定性祝旷。使用線程池可以進(jìn)行統(tǒng)一的分配、調(diào)優(yōu)和監(jiān)控。
④提供更多更加強(qiáng)大的功能:線程池具備可擴(kuò)展性怀跛,允許開發(fā)人員向其中增加更多的功能。比如延時(shí)暫定線程池ScheduledThreadPoolExecutor柄冲,就允許任務(wù)延遲執(zhí)行或者定期執(zhí)行吻谋。
(2)線程池解決的問題是什么?
線程池解決的核心問題就是資源管理問題现横。在并發(fā)環(huán)境下漓拾,系統(tǒng)不確定在任意時(shí)刻中,有多少任務(wù)需要執(zhí)行戒祠,有多少資源需要投入骇两。這種不確定性將帶來以下若干問題:
1.頻繁申請(qǐng)/銷毀資源和調(diào)度資源,將帶來額外的消耗姜盈,可能會(huì)非常巨大
2.對(duì)資源無限申請(qǐng)缺少抑制手段低千,易引發(fā)系統(tǒng)資源耗盡的風(fēng)險(xiǎn)。
3.系統(tǒng)無法合理管理內(nèi)部的資源分布馏颂,會(huì)降低系統(tǒng)的穩(wěn)定性示血。
為了解決資源分配問題,線程池采用"池化"(Pooling)思想救拉。池化难审,顧名思義,是為了最大化收益并最小化風(fēng)險(xiǎn)亿絮,而將資源統(tǒng)一在一起管理的一種思想告喊。
池化思想在計(jì)算機(jī)中還有其他的應(yīng)用:
①內(nèi)存池:預(yù)先申請(qǐng)內(nèi)存,提升申請(qǐng)內(nèi)存速度派昧,減少內(nèi)存碎片黔姜。
②連接池:預(yù)先申請(qǐng)數(shù)據(jù)庫連接,提升申請(qǐng)連接的速度斗锭,降低系統(tǒng)的開銷
③實(shí)例池:循環(huán)使用對(duì)象地淀,減少資源在初始化和釋放時(shí)的昂貴損耗。
(3)線程池的核心設(shè)計(jì)是怎樣的岖是?
①ThreadPoolExecutor頂層接口是Executor帮毁,頂層接口Executor提供了一種思想:將任務(wù)提交和任務(wù)執(zhí)行進(jìn)行解耦。用戶無需關(guān)注如何創(chuàng)建線程豺撑、如何調(diào)度線程來執(zhí)行任務(wù)烈疚,用戶只需提供Runnable對(duì)象,將任務(wù)的執(zhí)行邏輯提交到執(zhí)行器Executor中聪轿,由Executor框架完成線程的調(diào)配和任務(wù)的執(zhí)行部分爷肝。
②ExecutorService接口增加了一些能力:擴(kuò)充執(zhí)行任務(wù)的的能力,補(bǔ)充可以為一個(gè)活一批任務(wù)生成Future的方法;提供了管控線程池的方法灯抛,比如停止線程池的運(yùn)行(shutDown/shutDownNow/isShutDown)
③AbstractExecutorService則是上層的抽象類金赦,將執(zhí)行任務(wù)的流程串聯(lián)起來,保證下層的實(shí)現(xiàn)只需關(guān)注一個(gè)執(zhí)行任務(wù)的方法接口对嚼。實(shí)現(xiàn)了submit相關(guān)方法夹抗。
④最下層的實(shí)現(xiàn)類ThreadPoolExecutor實(shí)現(xiàn)最復(fù)雜的運(yùn)行部分,ThreadPoolExecutor一方面維護(hù)自身的生命周期纵竖,另一方面同時(shí)管理線程和任務(wù)漠烧,使兩者良好的結(jié)合從而執(zhí)行并行任務(wù)。
線程池在內(nèi)部實(shí)際上構(gòu)建了一個(gè)生產(chǎn)者消費(fèi)者模型靡砌,將線程和任務(wù)兩者解耦已脓,并不直接關(guān)聯(lián),從而良好的緩沖任務(wù)通殃,復(fù)用線程度液。線程池的運(yùn)行主要分成兩部分:任務(wù)管理、線程管理邓了。任務(wù)管理部分充當(dāng)生產(chǎn)者的角色恨诱,當(dāng)任務(wù)提交后,線程池會(huì)判斷該任務(wù)后續(xù)的流轉(zhuǎn):(1)直接申請(qǐng)線程執(zhí)行該任務(wù)骗炉;(2)緩沖到隊(duì)列中等待線程執(zhí)行照宝;(3)拒絕該任務(wù)。線程管理部分是消費(fèi)者句葵,它們被統(tǒng)一維護(hù)在線程池內(nèi)厕鹃,根據(jù)任務(wù)請(qǐng)求進(jìn)行線程的分配,當(dāng)線程執(zhí)行完任務(wù)后則會(huì)繼續(xù)獲取新的任務(wù)去執(zhí)行乍丈,最終當(dāng)線程獲取不到任務(wù)的時(shí)候剂碴,線程就會(huì)被回收。
(4)線程池有哪些狀態(tài)轻专?
RUNNING:接受新任務(wù)并處理排隊(duì)的任務(wù)忆矛。
SHUTDOWN:不接受新任務(wù),但處理排隊(duì)的任務(wù)请垛。
STOP:不接受新任務(wù)催训,不處理排隊(duì)的任務(wù),并中斷正在進(jìn)行的任務(wù)宗收。
TIDYING:所有任務(wù)都已終止漫拭,workerCount 為零,線程轉(zhuǎn)換到 TIDYING 狀態(tài)將運(yùn)行 terminated() 鉤子方法混稽。
TERMINATED:terminated() 已完成
(5)線程池里有個(gè)ctl采驻,你知道它是如何設(shè)計(jì)的嗎审胚?
ctl是一個(gè)打包兩個(gè)概念字段的AtomicInteger.
①workerCount:指示線程的有效數(shù)量
②runState:指示線程池的運(yùn)行狀態(tài),有RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED狀態(tài)
int類型占4字節(jié)有32位礼旅,其中ctl的低29位表示workerCount膳叨,高3位用于表示runState。
ctl為什么這么設(shè)計(jì)各淀,有什么好處嗎懒鉴?
主要好處是將runState和workerCount的操作封裝成一個(gè)原子操作。
runState和workerCount是線程池正常運(yùn)轉(zhuǎn)中的2個(gè)最重要的屬性碎浇,線程池在某一時(shí)刻該做什么操作,取決于這2個(gè)屬性的值璃俗。
無論是查詢還是修改奴璃,我們必須保證對(duì)這2個(gè)屬性的操作是屬于"同一時(shí)刻"的,也就是原子操作城豁,否則出現(xiàn)錯(cuò)亂的情況苟穆。如果使用2個(gè)變量分別存儲(chǔ),要保證原子性則需要額外進(jìn)行加鎖操作唱星,這顯然會(huì)帶來額外的開銷雳旅,而將這2個(gè)變量封裝成1個(gè)AtomicInteger則不會(huì)帶來額外的加鎖開銷,而且只需要使用簡(jiǎn)單的位操作就能分別得到runState和workerCount间聊。
通過ctl得到runState攒盈,只需要通過位操作:ctl & ~CAPACITY。
通過 ctl 得到 workerCount 則更簡(jiǎn)單了哎榴,只需通過位操作:c & CAPACITY型豁。
(6)任務(wù)調(diào)度流程
首先檢測(cè)線程池運(yùn)行狀態(tài)瘩燥,如果不是RUNNING宰译,則直接拒絕,線程池要保證在RUNNING的狀態(tài)下執(zhí)行任務(wù)留潦。
如果workerCount < corePoolSize飘言,則創(chuàng)建并啟動(dòng)一個(gè)線程來執(zhí)行新提交的任務(wù)衣形。
如果workerCount >= corePoolSize,且線程池內(nèi)的阻塞隊(duì)列未滿姿鸿,則將任務(wù)添加到該阻塞隊(duì)列中谆吴。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且線程池內(nèi)的阻塞隊(duì)列已滿般妙,則創(chuàng)建并啟動(dòng)一個(gè)線程來執(zhí)行新提交的任務(wù)纪铺。
如果workerCount >= maximumPoolSize,并且線程池內(nèi)的阻塞隊(duì)列已滿, 則根據(jù)拒絕策略來處理該任務(wù), 默認(rèn)的處理方式是直接拋異常碟渺。
這些問題多摘自上面美團(tuán)技術(shù)博客和知乎專欄鲜锚,更多的問題參考上面鏈接突诬。
四、協(xié)程
枯燥的Kotlin協(xié)程三部曲(中)
真正的協(xié)程
①一種非搶占式/協(xié)作式 的任務(wù)調(diào)度模式芜繁,程序可主動(dòng)掛起或恢復(fù)執(zhí)行
②基于線程旺隙,相對(duì)于線程輕量很多,可理解為用戶層模擬線程操作
③上下文切換由用戶控制骏令,避免大量中斷參與蔬捷,減少線程上下文切換
kotlin中的假協(xié)程
語言級(jí)別并沒有實(shí)現(xiàn)一種同步機(jī)制(鎖),還是依靠kotlin-jvm提供的java關(guān)鍵字榔袋,鎖的實(shí)現(xiàn)還是交給線程處理周拐,因此Kotlin的協(xié)程本質(zhì)上只是一套【基于原生Java Thread API的封裝】。只是這套API隱藏了異步實(shí)現(xiàn)細(xì)節(jié)凰兑,讓我們可以用【同步的方法來寫異步操作】罷了妥粟。
使用demo:
Kotlin-JVM的協(xié)程是假協(xié)程,只是對(duì)底層Thread的一次良好封裝吏够,通過在協(xié)程中使用
Thread.currentThread.name
可以在協(xié)程中打印出當(dāng)前線程的名字勾给,這里可以看到這個(gè)協(xié)程所用的線程名字為:DefaultDispatcher-worker-1。盲猜是線程池锅知,畢竟高效的多線程調(diào)度基本離不開線程池播急。26行比21行先執(zhí)行的原因是:創(chuàng)建線程池需要費(fèi)點(diǎn)時(shí)間,所以協(xié)程里的代碼沒有主線程同步代碼里的執(zhí)行速度快售睹。
delay()是一個(gè)人掛起函數(shù)桩警,可在不堵塞線程的情況下延遲協(xié)程;Thread.sleep()則會(huì)堵塞當(dāng)前線程侣姆。
這里的Thread.sleep(2000L)是不能去掉的生真,這里需要阻塞主線程來保證JVM存活。否則就會(huì)出現(xiàn)下面這種情況:
阻塞與非阻塞
delay()是非阻塞的捺宗;
Thread.sleep()是阻塞的柱蟀;
runBlocking{}是阻塞的:
調(diào)用runBlocking的主線程會(huì)一直阻塞直到runBlocking內(nèi)部的協(xié)程執(zhí)行完畢。
runBlocking是一個(gè)全局函數(shù)蚜厉,可在任意地方調(diào)用长已,不過 項(xiàng)目中用得不多,畢竟堵塞main線程意義不大昼牛,常用于單元測(cè)試防止JVM退出术瓮。
協(xié)程作用域:CoroutineScope
在 Android 環(huán)境中,通常每個(gè)界面(Activity贰健、Fragment 等)啟動(dòng)的 Coroutine 只在該界面有意義胞四,如果用戶在等待 Coroutine 執(zhí)行的時(shí)候退出了這個(gè)界面,則再繼續(xù)執(zhí)行這個(gè) Coroutine 可能是沒必要的伶椿。另外 Coroutine 也需要在適當(dāng)?shù)?context 中執(zhí)行辜伟,否則會(huì)出現(xiàn)錯(cuò)誤氓侧,比如在非 UI 線程去訪問 View。 所以 Coroutine 在設(shè)計(jì)的時(shí)候导狡,要求在一個(gè)范圍(Scope)內(nèi)執(zhí)行约巷,這樣當(dāng)這個(gè) Scope 取消的時(shí)候,里面所有的子 Coroutine 也自動(dòng)取消旱捧。所以要使用 Coroutine 必須要先創(chuàng)建一個(gè)對(duì)應(yīng)的 CoroutineScope独郎。
CoroutineScope是一個(gè)接口,其內(nèi)部只定義了一個(gè)變量:coroutineContext 枚赡,也就是協(xié)程的上下文:
CoroutineScope只是定義了一個(gè)新的協(xié)程的作用域氓癌。每一個(gè)協(xié)程的builder(launch、async等)都是CoroutineScope的擴(kuò)展方法贫橙,并且自動(dòng)的繼承了當(dāng)前作用域的coroutineContext(協(xié)程上下文環(huán)境)和取消操作顽铸。
雖然CouroutineScope只有一個(gè)屬性,但是卻有很多擴(kuò)展函數(shù)和擴(kuò)展屬性料皇,比如launc/async/cancel等。
協(xié)程作用域可分為以下幾種:
1.全局作用域:GlobalScope
可以看到GlobalScope是一個(gè)單例對(duì)象星压,生命周期貫穿整個(gè)JVM践剂,使用的時(shí)候需要注意內(nèi)存泄露。一般而言不會(huì)直接使用GlobalScope來創(chuàng)建協(xié)程娜膘。
2.自定義作用域
有兩種方法:
①需要我們?cè)贏ctivity后者Fragment中實(shí)現(xiàn)CoroutineScope接口逊脯,實(shí)現(xiàn)協(xié)程上下文。同時(shí)需要在onDestroy中取消掉協(xié)程竣贪,防止內(nèi)存泄露军洼。
②使用MainScope()函數(shù)
為了在Android場(chǎng)景中更方便的使用,官方提供了MainScope()函數(shù)快速創(chuàng)建基于主線程協(xié)程作用域演怎。
如下圖(并不能運(yùn)行哈匕争,只是一個(gè)示例,沒有上下文環(huán)境)
③使用coroutineScope()和supervisorScope()創(chuàng)建子作用域
作用域函數(shù)
創(chuàng)建協(xié)程函數(shù)
有兩種創(chuàng)建協(xié)程的方式:
1.launch:
launch返回一個(gè)Job爷耀,用于協(xié)程監(jiān)督與取消甘桑,用于無返回值的場(chǎng)景。
可以通過Job的start歹叮、cancel跑杭、join等方法來控制協(xié)程的啟動(dòng)和取消。
2.async:
async返回一個(gè)Job的子類Deferred,可通過await()獲取完成時(shí)返回值咆耿。
如下:
掛起函數(shù):suspend關(guān)鍵字
Kotlin協(xié)程提供了suspend關(guān)鍵字德谅,用于定義一個(gè)掛起函數(shù),它就是一個(gè)標(biāo)記萨螺。
當(dāng)我們寫的普通函數(shù)需要在某些時(shí)刻掛起和恢復(fù),加上這個(gè)關(guān)鍵字就行窄做。
其真正作用是:
告知編譯期愧驱,這個(gè)函數(shù)需要在協(xié)程中執(zhí)行。編譯器會(huì)將掛起函數(shù)用有限狀態(tài)機(jī)轉(zhuǎn)化為一種優(yōu)化版的回調(diào)浸策。
如上圖冯键,如果不加suspend,那么在這個(gè)方法中是不能使用delay方法的庸汗。
Job
調(diào)用launch函數(shù)會(huì)返回一個(gè)Job對(duì)象惫确,代表一個(gè)協(xié)程的工作任務(wù)。
/**
* 協(xié)程狀態(tài)
*/
isActive: Boolean //是否存活
isCancelled: Boolean //是否取消
isCompleted: Boolean //是否完成
children: Sequence<Job> // 所有子作業(yè)
/**
* 協(xié)程控制
*/
cancel() // 取消協(xié)程
join() // 堵塞當(dāng)前線程直到協(xié)程執(zhí)行完畢
cancelAndJoin() // 兩者結(jié)合蚯舱,取消并等待協(xié)程完成
cancelChildren() // 取消所有子協(xié)程改化,可傳入CancellationException作為取消原因
attachChild(child: ChildJob) // 附加一個(gè)子協(xié)程到當(dāng)前協(xié)程上
Job的生命周期包括一系列狀態(tài):
New(新創(chuàng)建)/Active(活躍)/Completing(完成中)
異常處理
Kotlin中異常處理有三種:
1.try-catch直接捕獲作用域內(nèi)異常:
無法使用try-catch捕獲launch和async作用域的異常
2.全局異常處理(throw)
3.異常傳播【關(guān)鍵點(diǎn)】
協(xié)程作用域中異常傳播默認(rèn)是雙向的,其表現(xiàn)為:
①父協(xié)程發(fā)生異常枉昏,所有子協(xié)程都會(huì)取消
②子協(xié)程發(fā)生異常陈肛,會(huì)導(dǎo)致父協(xié)程取消,間接導(dǎo)致這個(gè)子協(xié)程的兄弟協(xié)程也取消兄裂。
有兩種方式可以將異常傳播變?yōu)閱蜗?/strong>句旱,即子協(xié)程發(fā)生異常不會(huì)影響父協(xié)程及兄弟協(xié)程。其中一種方式是:
①用SupervisorJob代替Job
②另一種是使用自定義作用域函數(shù)supervisorScope
啟動(dòng)模式
launch&async第二個(gè)參數(shù)CoroutineStart,可以指定協(xié)程的啟動(dòng)模式:
協(xié)程的啟動(dòng)模式有四種:
// 默認(rèn)晰奖,創(chuàng)建后立即開始調(diào)度谈撒,調(diào)度前被取消,直接進(jìn)入取消響應(yīng)狀態(tài)匾南。
DEFAULT,
// 懶加載啃匿,不會(huì)立即開始調(diào)度,需要手動(dòng)調(diào)用start蛆楞、join或await才會(huì)
// 開始調(diào)度溯乒,如果調(diào)度前就被取消,協(xié)程將直接進(jìn)入異常結(jié)束狀態(tài)豹爹。
LAZY,
// 和Default類似裆悄,立即開始調(diào)度,在執(zhí)行到一個(gè)掛起函數(shù)前不響應(yīng)取消帅戒。
// 涉及到cancle才有意義
@ExperimentalCoroutinesApi
ATOMIC,
// 直接在當(dāng)前線程執(zhí)行協(xié)程體灯帮,直到遇到第一個(gè)掛起函數(shù),才會(huì)調(diào)度到
// 指定調(diào)度器所在的線程上執(zhí)行
@ExperimentalCoroutinesApi
UNDISPATCHED;
協(xié)程調(diào)度器:CoroutineDispatcher
1.一種有四種:
- Default : 默認(rèn)逻住,線程池钟哥,適合處理后臺(tái)計(jì)算,CPU密集型任務(wù)調(diào)度器
- IO : IO調(diào)度器瞎访,適合執(zhí)行IO相關(guān)操作腻贰,IO密集型任務(wù)調(diào)度器
- MAIN : UI調(diào)度器,根據(jù)平臺(tái)不同會(huì)初始化為對(duì)應(yīng)UI線程的調(diào)度器扒秸,如Android的主線程
- Unconfined : 不指定線程播演,如果子協(xié)程切換線程冀瓦,接下來的代碼也在該線程繼續(xù)
2.withContext
和launch/async/runBlocking不同的是,withContext不會(huì)創(chuàng)建新的協(xié)程写烤,常用于切換代碼執(zhí)行所運(yùn)行的【線程】翼闽。它也是一個(gè)掛起方法,直到結(jié)束返回結(jié)果洲炊。多個(gè)withContext是串行執(zhí)行的感局,所以很適合那種一個(gè)任務(wù)依賴上一個(gè)任務(wù)返回結(jié)果的情況。
五暂衡、ThreadLocal
ThreadLocal 使用與原理
線程局部變量询微。ThreadLocal設(shè)計(jì)的目的就是為了能夠在當(dāng)前線程中有屬于自己的變量。當(dāng)使用ThreadLocal維護(hù)變量時(shí)狂巢,ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本撑毛,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本唧领。
應(yīng)用場(chǎng)景:
當(dāng)很多線程需要多次使用同一個(gè)對(duì)象藻雌,并且需要該對(duì)象具有相同初始化值的時(shí)候最適合使用ThreadLocal。
ThreadLocal和synchronized的異同
①進(jìn)行同步控制synchronized 效率降低 并發(fā)變同步(串行)
②使用ThreadLocal 本地線程 每個(gè)線程一個(gè)變量副本(各不相干斩个,提升并發(fā)效率)
概括起來說蹦疑,對(duì)于多線程資源共享的問題,同步機(jī)制采用了“以時(shí)間換空間”的方式萨驶,而ThreadLocal采用了“以空間換時(shí)間”的方式。前者僅提供一份變量艇肴,讓不同的線程排隊(duì)訪問腔呜,而后者為每一個(gè)線程都提供了一份變量,因此可以同時(shí)訪問而互不影響再悼。
ThreadLocal原理解析
①每個(gè)Thread對(duì)象內(nèi)部都維護(hù)了一個(gè)ThreadLocal的Map:ThreadLocalMap核畴,這個(gè)map可以存放多個(gè)ThreadLocal。
②當(dāng)我們調(diào)用get()方法時(shí)冲九,先獲取當(dāng)前線程谤草,然后獲取當(dāng)前線程的ThreadLocalMap對(duì)象。如果非空就取出ThreadLocal的value莺奸,否則進(jìn)行初始化丑孩,初始化就是將initialValue的值set到ThreadLocal中壹瘟。
③當(dāng)我們調(diào)用set()方法時(shí)纫事,就是拿到當(dāng)前的線程對(duì)應(yīng)的ThreadLocal霉颠,不為空碟摆,賦值明场。為空,創(chuàng)建ThreadLocalMap綁定Thread并賦值慰丛。
④ThreadLocalMap是以弱引用的ThreadLocal為key的擎析,不是Thread!
⑤總結(jié):當(dāng)我們調(diào)用get方法的時(shí)候轧拄,其實(shí)每個(gè)當(dāng)前線程中都有一個(gè)ThreadLocal揽祥。每次獲取或者設(shè)置都是對(duì)該ThreadLocal進(jìn)行的操作,是與其他線程分開的檩电。
ThreadLocal內(nèi)存泄漏
ThreaLocal內(nèi)存泄漏
threadLocalMap使用ThreadLocal的弱引用作為key拄丰,如果一個(gè)ThreadLocal不存在外部強(qiáng)引用時(shí),Key(ThreadLocal)勢(shì)必會(huì)被GC回收是嗜,這樣就會(huì)導(dǎo)致ThreadLocalMap中key為null愈案, 而value還存在著強(qiáng)引用,只有thead線程退出以后,value的強(qiáng)引用鏈條才會(huì)斷掉鹅搪。
但如果當(dāng)前線程再遲遲不結(jié)束的話站绪,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠(yuǎn)無法回收,造成內(nèi)存泄漏丽柿。
ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng)恢准,如果沒有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?br>
ThreadLocal正確用法
①每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)
②將ThreadLocal變量定義成private static甫题,這樣就一直存在ThreadLocal的強(qiáng)引用馁筐,也就能保證任何時(shí)候都能通過ThreadLocal的弱引用訪問到Entry的value值,進(jìn)而清除掉 坠非∶舫粒【沒get到。炎码。盟迟。】
package com.dawn.zgstep.threads.objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadLocal {
private static int count = 0;
ThreadLocal<String> threadLocal = new ThreadLocal<>();
private void testThreadLocal() {
ExecutorService executorService = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
int finalI = i;
executorService.submit(() -> {
threadLocal.set(Thread.currentThread().getName());
soutLocal();
});
}
}
private void soutLocal(){
System.out.println(threadLocal.get());
}
public static void main(String[] args) {
TestThreadLocal testThreadLocal = new TestThreadLocal();
testThreadLocal.testThreadLocal();
}
}
//結(jié)果:
pool-1-thread-1
pool-1-thread-3
pool-1-thread-5
pool-1-thread-2
pool-1-thread-4
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10
pool-1-thread-11
pool-1-thread-12
pool-1-thread-13
pool-1-thread-14
pool-1-thread-15
pool-1-thread-16
pool-1-thread-17
pool-1-thread-18
pool-1-thread-19
pool-1-thread-20
總結(jié):
①每個(gè)線程都有一個(gè)ThreadLocalMap 類型的 threadLocals 屬性潦闲。
②ThreadLocalMap 類相當(dāng)于一個(gè)Map攒菠,key 是 ThreadLocal 本身,value 就是我們的值歉闰。
③當(dāng)我們通過 threadLocal.set(new Integer(123)); 辖众,我們就會(huì)在這個(gè)線程中的 threadLocals 屬性中放入一個(gè)鍵值對(duì),key 是這個(gè)threadLocal.set(new Integer(123)) 的 threadlocal和敬,value就是值new Integer(123)凹炸。
④當(dāng)我們通過 threadlocal.get() 方法的時(shí)候,首先會(huì)根據(jù)這個(gè)線程得到這個(gè)線程的 threadLocals 屬性昼弟,然后由于這個(gè)屬性放的是鍵值對(duì)还惠,我們就可以根據(jù)鍵 threadlocal 拿到值。 注意,這時(shí)候這個(gè)鍵 threadlocal 和 我們 set 方法的時(shí)候的那個(gè)鍵 threadlocal 是一樣的蚕键,所以我們能夠拿到相同的值救欧。
⑤ThreadLocalMap 的get/set/remove方法跟HashMap的內(nèi)部實(shí)現(xiàn)都基本一樣,通過 "key.threadLocalHashCode & (table.length - 1)" 運(yùn)算式計(jì)算得到我們想要找的索引位置锣光,如果該索引位置的鍵值對(duì)不是我們要找的笆怠,則通過nextIndex方法計(jì)算下一個(gè)索引位置,直到找到目標(biāo)鍵值對(duì)或者為空誊爹。
⑥hash沖突:在HashMap中相同索引位置的元素以鏈表形式保存在同一個(gè)索引位置蹬刷;而在ThreadLocalMap中,沒有使用鏈表的數(shù)據(jù)結(jié)構(gòu)频丘,而是將(當(dāng)前的索引位置+1)對(duì)length取模的結(jié)果作為相同索引元素的位置:源碼中的nextIndex方法办成,可以表達(dá)成如下公式:如果i為當(dāng)前索引位置,則下一個(gè)索引位置 = (i + 1 < len) ? i + 1 : 0搂漠。
六迂卢、網(wǎng)絡(luò)相關(guān)
必看網(wǎng)絡(luò)面試1
這個(gè)挺詳細(xì),必須得看
網(wǎng)絡(luò)相關(guān)查漏補(bǔ)缺
比如狀態(tài)碼等桐汤。
下面一些問題是上面提的不全面的而克,這里做一下簡(jiǎn)單總結(jié)
1.Http1.0、Http1.1怔毛、Http2.0區(qū)別
參考資料
HTTP1.0最早在網(wǎng)頁中使用是在1996年员萍,那個(gè)時(shí)候只是使用一些較為簡(jiǎn)單的網(wǎng)頁上和網(wǎng)絡(luò)請(qǐng)求上,而HTTP1.1則在1999年才開始廣泛應(yīng)用于現(xiàn)在的各大瀏覽器網(wǎng)絡(luò)請(qǐng)求中拣度,同時(shí)HTTP1.1也是當(dāng)前使用最為廣泛的HTTP協(xié)議碎绎。
主要區(qū)別:
①帶寬優(yōu)化及網(wǎng)絡(luò)連接的使用:
Http1.1在請(qǐng)求頭引入了range頭域,允許只請(qǐng)求資源的某個(gè)部分抗果,返回碼是206.這是我們斷點(diǎn)續(xù)傳的基礎(chǔ)混卵!
②新增了24個(gè)錯(cuò)誤狀態(tài)響應(yīng)碼。比如409表示請(qǐng)求的資源與資源的當(dāng)前狀態(tài)發(fā)生沖突窖张;410表示服務(wù)器上的某個(gè)資源被永久刪除。
③ Host頭處理蚁滋;Http1.0中認(rèn)為每臺(tái)服務(wù)器都綁定一個(gè)唯一的ip宿接。但隨后的虛擬主機(jī)的發(fā)展,多個(gè)虛擬主機(jī)共享一個(gè)IP地址辕录。HTTP1.1的請(qǐng)求消息和響應(yīng)消息都應(yīng)支持Host頭域睦霎,且請(qǐng)求消息中如果沒有Host頭域會(huì)報(bào)告一個(gè)錯(cuò)誤
④長(zhǎng)連接。Http1.1支持長(zhǎng)連接和請(qǐng)求的流水線處理走诞。在一個(gè)TCP連接上可以傳送多個(gè)HTTP請(qǐng)求和響應(yīng)副女,減少了建立和關(guān)閉連接的消耗和延遲。在Http1.1中默認(rèn)開啟Connection:keep-alive選項(xiàng)蚣旱,一定程度上彌補(bǔ)了Http1.0每次請(qǐng)求都要?jiǎng)?chuàng)建連接的缺點(diǎn)碑幅。
HTTP2.0和HTTP1.X相比的新特性:
新的二進(jìn)制格式:HTTP2.0的協(xié)議解析決定采用二進(jìn)制格式戴陡,實(shí)現(xiàn)方便且健壯,不同于HTTP1.x的解析是基于文本
多路復(fù)用:連接共享沟涨,即每一個(gè)request都是是用作連接共享機(jī)制的恤批。一個(gè)request對(duì)應(yīng)一個(gè)id,這樣一個(gè)連接上可以有多個(gè)request裹赴。
服務(wù)端推送:服務(wù)器主動(dòng)向客戶端推送消息
2.HTTPS與HTTP的一些區(qū)別
HTTPS協(xié)議需要到CA申請(qǐng)證書喜庞,一般免費(fèi)證書很少,需要交費(fèi)棋返。
HTTP協(xié)議運(yùn)行在TCP之上延都,所有傳輸?shù)膬?nèi)容都是明文,HTTPS運(yùn)行在SSL/TLS之上睛竣,SSL/TLS運(yùn)行在TCP之上晰房,所有傳輸?shù)膬?nèi)容都經(jīng)過加密的。
HTTP和HTTPS使用的是完全不同的連接方式酵颁,用的端口也不一樣嫉你,前者是80,后者是443躏惋。
HTTPS可以有效的防止運(yùn)營商劫持幽污,解決了防劫持的一個(gè)大問題。
3.TCP和UDP
TCP傳輸控制協(xié)議:面向連接簿姨;使用全雙工的可靠信道距误;提供可靠的服務(wù),即無差錯(cuò)扁位、不丟失准潭、不重復(fù)且按序到達(dá);擁塞控制域仇、流量控制刑然、超時(shí)重發(fā)、丟棄重復(fù)數(shù)據(jù)等等可靠性檢測(cè)手段暇务;面向字節(jié)流泼掠;每條TCP連接只能是點(diǎn)到點(diǎn)的;用于傳輸可靠性要求高的數(shù)據(jù)
UDP用戶數(shù)據(jù)報(bào)協(xié)議:無連接垦细;使用不可靠信道择镇;盡最大努力交付,即不保證可靠交付括改;無擁塞控制等腻豌;面向報(bào)文;支持一對(duì)一、一對(duì)多吝梅、多對(duì)一和多對(duì)多的交互通信虱疏;用于傳輸可靠性要求不高的數(shù)據(jù)
TCP的三次握手、四次揮手
參考博客
TCP提供了一種可靠憔涉、面向連接订框、字節(jié)流、傳輸層的服務(wù)兜叨,采用三次握手建立一個(gè)連接穿扳。采用4次揮手來關(guān)閉一個(gè)連接。
三次握手
第一次握手:客戶端給服務(wù)端發(fā)一個(gè) SYN 報(bào)文国旷,并指明客戶端的初始化序列號(hào) ISN(c)矛物。此時(shí)客戶端處于 SYN_SEND 狀態(tài)。
首部的同步位SYN=1跪但,初始序號(hào)seq=x履羞,SYN=1的報(bào)文段不能攜帶數(shù)據(jù),但要消耗掉一個(gè)序號(hào)屡久。
第二次握手:服務(wù)器收到客戶端的 SYN 報(bào)文之后忆首,會(huì)以自己的 SYN 報(bào)文作為應(yīng)答,并且指定了自己的初始化序列號(hào) ISN(s)被环。同時(shí)會(huì)把客戶端的 ISN + 1 作為ACK 的值糙及,表示自己已經(jīng)收到了客戶端的 SYN,此時(shí)服務(wù)器處于 SYN_RCVD 的狀態(tài)筛欢。
在確認(rèn)報(bào)文段中SYN=1浸锨,ACK=1,確認(rèn)號(hào)ack=x+1版姑,初始序號(hào)seq=y柱搜。
第三次握手:客戶端收到 SYN 報(bào)文之后,會(huì)發(fā)送一個(gè) ACK 報(bào)文剥险,當(dāng)然聪蘸,也是一樣把服務(wù)器的 ISN + 1 作為 ACK 的值,表示已經(jīng)收到了服務(wù)端的 SYN 報(bào)文表制,此時(shí)客戶端處于 ESTABLISHED 狀態(tài)健爬。服務(wù)器收到 ACK 報(bào)文之后,也處于 ESTABLISHED 狀態(tài)夫凸,此時(shí),雙方已建立起了連接阱持。
確認(rèn)報(bào)文段ACK=1夭拌,確認(rèn)號(hào)ack=y+1,序號(hào)seq=x+1(初始為seq=x,第二個(gè)報(bào)文段所以要+1)鸽扁,ACK報(bào)文段可以攜帶數(shù)據(jù)蒜绽,不攜帶數(shù)據(jù)則不消耗序號(hào)。
問:為什么需要三次桶现,兩次不行嗎躲雅?
假如現(xiàn)在客戶端想向服務(wù)端進(jìn)行握手,它發(fā)送了第一個(gè)連接的請(qǐng)求報(bào)文骡和,但是由于網(wǎng)絡(luò)信號(hào)差或者服務(wù)器負(fù)載過多相赁,這個(gè)請(qǐng)求沒有立即到達(dá)服務(wù)端,而是在某個(gè)網(wǎng)絡(luò)節(jié)點(diǎn)中長(zhǎng)時(shí)間的滯留了慰于,以至于滯留到客戶端連接釋放以后的某個(gè)時(shí)間點(diǎn)才到達(dá)服務(wù)端钮科,那么這就是一個(gè)失效的報(bào)文,但是服務(wù)端接收到這個(gè)失效的請(qǐng)求報(bào)文后婆赠,就誤認(rèn)為客戶端又發(fā)了一次連接請(qǐng)求绵脯,服務(wù)端就會(huì)想向客戶端發(fā)出確認(rèn)的報(bào)文,表示同意建立連接剩檀。
假如不采用三次握手逆屡,那么只要服務(wù)端發(fā)出確認(rèn)淤袜,表示新的建立就連接了。但是現(xiàn)在客戶端并沒有發(fā)出建立連接的請(qǐng)求悴侵,其實(shí)這個(gè)請(qǐng)求是失效的請(qǐng)求,一切都是服務(wù)端在自相情愿废境,因此客戶端是不會(huì)理睬服務(wù)端的確認(rèn)信息畜挨,也不會(huì)向服務(wù)端發(fā)送確認(rèn)的請(qǐng)求,但是服務(wù)器卻認(rèn)為新的連接已經(jīng)建立起來了噩凹,并一直等待客戶端發(fā)來數(shù)據(jù)巴元,這樣的情況下,服務(wù)端的很多資源就沒白白浪費(fèi)掉了
四次握手
為什么建立連接是三次驮宴,斷掉連接是四次呢逮刨?
我想可能是因?yàn)橄嘁姇r(shí)難別亦難吧。
剛開始雙方都處于 ESTABLISHED 狀態(tài)堵泽,假如是客戶端先發(fā)起關(guān)閉請(qǐng)求:
第一次揮手:客戶端發(fā)送一個(gè) FIN 報(bào)文修己,報(bào)文中會(huì)指定一個(gè)序列號(hào)。此時(shí)客戶端處于 FIN_WAIT1 狀態(tài)迎罗。
即發(fā)出連接釋放報(bào)文段(FIN=1睬愤,序號(hào)seq=u),并停止再發(fā)送數(shù)據(jù)纹安,主動(dòng)關(guān)閉TCP連接尤辱,進(jìn)入FIN_WAIT1(終止等待1)狀態(tài)砂豌,等待服務(wù)端的確認(rèn)。
第二次揮手:服務(wù)端收到 FIN 之后光督,會(huì)發(fā)送 ACK 報(bào)文阳距,且把客戶端的序列號(hào)值 +1 作為 ACK 報(bào)文的序列號(hào)值,表明已經(jīng)收到客戶端的報(bào)文了结借,此時(shí)服務(wù)端處于 CLOSE_WAIT 狀態(tài)筐摘。
即服務(wù)端收到連接釋放報(bào)文段后即發(fā)出確認(rèn)報(bào)文段(ACK=1,確認(rèn)號(hào)ack=u+1船老,序號(hào)seq=v)咖熟,服務(wù)端進(jìn)入CLOSE_WAIT(關(guān)閉等待)狀態(tài),此時(shí)的TCP處于半關(guān)閉狀態(tài)努隙,客戶端到服務(wù)端的連接釋放球恤。客戶端收到服務(wù)端的確認(rèn)后荸镊,進(jìn)入FIN_WAIT2(終止等待2)狀態(tài)咽斧,等待服務(wù)端發(fā)出的連接釋放報(bào)文段。
第三次揮手:如果服務(wù)端也想斷開連接了躬存,和客戶端的第一次揮手一樣张惹,發(fā)給 FIN 報(bào)文,且指定一個(gè)序列號(hào)岭洲。此時(shí)服務(wù)端處于 LAST_ACK 的狀態(tài)宛逗。(中間多了這一步,所以是四次)
即服務(wù)端沒有要向客戶端發(fā)出的數(shù)據(jù)盾剩,服務(wù)端發(fā)出連接釋放報(bào)文段(FIN=1雷激,ACK=1,序號(hào)seq=w告私,確認(rèn)號(hào)ack=u+1)屎暇,服務(wù)端進(jìn)入LAST_ACK(最后確認(rèn))狀態(tài),等待客戶端的確認(rèn)驻粟。
第四次揮手:客戶端收到 FIN 之后根悼,一樣發(fā)送一個(gè) ACK 報(bào)文作為應(yīng)答,且把服務(wù)端的序列號(hào)值 +1 作為自己 ACK 報(bào)文的序列號(hào)值蜀撑,此時(shí)客戶端處于 TIME_WAIT 狀態(tài)挤巡。需要過一陣子以確保服務(wù)端收到自己的 ACK 報(bào)文之后才會(huì)進(jìn)入 CLOSED 狀態(tài),服務(wù)端收到 ACK 報(bào)文之后酷麦,就處于關(guān)閉連接了矿卑,處于 CLOSED 狀態(tài)。
即客戶端收到服務(wù)端的連接釋放報(bào)文段后沃饶,對(duì)此發(fā)出確認(rèn)報(bào)文段(ACK=1母廷,seq=u+1瀑晒,ack=w+1),客戶端進(jìn)入TIME_WAIT(時(shí)間等待)狀態(tài)徘意。此時(shí)TCP未釋放掉,需要經(jīng)過時(shí)間等待計(jì)時(shí)器設(shè)置的時(shí)間2MSL后轩褐,客戶端才進(jìn)入CLOSED狀態(tài)椎咧。
七、Glide源碼解析
Glide源碼解析參考一
Glide源碼解析參考二把介,基于.9.0
截止目前為止:2021/5/17, Glide最新版本是4.12.0勤讽,本人以4.12.0進(jìn)行分析。
Glide架構(gòu)
可以看到拗踢,Glide采用五層架構(gòu)設(shè)計(jì)脚牍,從高到低依次是:
Request-->Engine-->Get Data-->Data-->Resource
按照邏輯功能劃分可以分為以下幾種:
Glide 是單例類,通過 Glide#get(Context) 方法可以獲取到實(shí)例巢墅。
Glide 類算是個(gè)全局的配置類诸狭,Encoder、Decoder君纫、ModelLoader驯遇、Pool 等等都在這里設(shè)置,此外還提供了創(chuàng)建 RequestManager 的接口(Glide#with() 方法)
使用Glide時(shí)最先調(diào)用Glide#with()方法創(chuàng)建RequestManager蓄髓,有五個(gè)重載方法;
如源碼注釋所說叉庐,RequestManager是Glide的請(qǐng)求管理類。
我們根據(jù)最上層的調(diào)用分析源碼:
1.with方法
首先判斷是否在主線程中調(diào)用会喝;如果是在子線程中調(diào)用陡叠,則調(diào)用get(activity.getApplicationContext()),也就是使用Application的Context。
with方法總結(jié):
通過RequestManagerRetriever的get獲取RequestManagerRetriever單例對(duì)象
通過retriever.get(context)獲取RequestManager肢执,在get(context)方法中通過對(duì)context類型的判斷做不同的處理:
context是Application枉阵,通過getApplicationManager(Context context) 創(chuàng)建并返回一個(gè)RequestManager對(duì)象
context是Activity,通過supportFragmentGet(activity, fm)在當(dāng)前activity創(chuàng)建并添加一個(gè)沒有界面的fragment蔚万,從而實(shí)現(xiàn)圖片加載與activity的生命周期相綁定岭妖,之后創(chuàng)建并返回一個(gè)RequestManager對(duì)象
最終在RequestManager中完成對(duì)Glide對(duì)象的初始化Glide.get(context)
在supportFragmentGet方法中,會(huì)調(diào)用Glide#get方法反璃,獲取Glide實(shí)例昵慌,此方法中會(huì)檢查glide字段是否為null,如果沒有初始化淮蜈,則調(diào)用initializeGlide方法初始化Glide斋攀。Glide#get方法里可以看到初始化邏輯是用的DCL單例來進(jìn)行初始化的。
Glide的創(chuàng)建是通過GlideBuilder也就是建造者模式創(chuàng)建的:
這個(gè)GlideBuilder#build方法是Glide創(chuàng)建的核心方法梧田,我們來仔細(xì)分析一下build方法:
可以看到build中首先新建了三個(gè)線程池:
1.sourceExecutor:負(fù)責(zé)處理內(nèi)存緩存(淳蔼?侧蘸?)
2.diskCacheExecutor:負(fù)責(zé)處理磁盤緩存
3.animationExecutor:負(fù)責(zé)加載Gif動(dòng)圖的每一幀圖像
這三個(gè)線程池底層都是GlideExecutor的build方法中通過ThreadPoolExecutor創(chuàng)建的 corePoolSize和maximumPoolSize都不相同:
接著創(chuàng)建了Bitmap池,也就是LruBitmapPool和BitmapPoolAdapter鹉梨;
以及數(shù)組池:LruArrayPool讳癌。
重頭戲來了,創(chuàng)建的memoryCache就是面試中常被問的緩存策略存皂。
可以看到Glide采用的是LruCache三級(jí)緩存晌坤。(這里注意這里的LruCache是Glide的類)
2.load()方法
創(chuàng)建完Glide對(duì)象接下來就是將圖片加載進(jìn)來了。load方法實(shí)際調(diào)用的就是RequestManager的load方法旦袋。返回一個(gè)RequestBuilder骤菠。
RequestBuilder用來構(gòu)建請(qǐng)求,例如設(shè)置RequestOptions疤孕、縮略圖商乎、加載失敗占位圖等。
這里返回的RequestBuilder使用的是克隆模式祭阀,用了一次深拷貝鹉戚。
深拷貝與淺拷貝的區(qū)別
3.into()方法
最重要的就是這個(gè)方法了。調(diào)用RequestBuilder的into方法:
into方法首先會(huì)檢測(cè)是否在主線程专控,如果不是在主線程中調(diào)用的into方法崩瓤,則直接拋出異常:IllegalArgumentException
接著構(gòu)建requestOptions。
這兩步完成后踩官,調(diào)用into的重載方法傳入創(chuàng)建好的ViewTarget以及主線程池(這是第四個(gè)線程池了却桶,前面創(chuàng)建Glide的時(shí)候,創(chuàng)建了三個(gè)線程池)蔗牡。
這個(gè)主線程池會(huì)將所有的任務(wù)都傳到主線程中進(jìn)行處理颖系。
在其重載的into方法中:
會(huì)創(chuàng)建Request,也就是封裝的請(qǐng)求辩越,這是個(gè)接口嘁扼,定義了對(duì)請(qǐng)求的開始、結(jié)束黔攒、狀態(tài)獲取趁啸、回收等操作。所以請(qǐng)求中不僅包含基本的信息督惰,還負(fù)責(zé)管理請(qǐng)求不傅。
Request的實(shí)現(xiàn)類有三個(gè):
①SingleRequest
②ThumbnailRequestCoordinator
③ErrorRequestCoordinator
我們依次分析。
1.SingleRequest
這個(gè)類負(fù)責(zé)執(zhí)行請(qǐng)求并將結(jié)果反映到Target上
當(dāng)我們使用 Glide 加載圖片時(shí)赏胚,會(huì)先根據(jù) Target 類型創(chuàng)建不同的 Target(比如ImageView會(huì)創(chuàng)建ViewTarget)访娶,然后 RequestBuilder 將這個(gè) target 當(dāng)做參數(shù)創(chuàng)建 Request 對(duì)象,Request 與 Target 就是這樣關(guān)聯(lián)起來的觉阅。
慮到性能問題崖疤,可能會(huì)連續(xù)創(chuàng)建很多個(gè) SingleRequest 對(duì)象秘车,所以使用了對(duì)象池來做緩存。
into方法里會(huì)調(diào)用Request的begin方法
當(dāng)我們調(diào)用Request#begin方法是并不會(huì)直接發(fā)起請(qǐng)求劫哼,而是等待ImageView初始化完成叮趴。對(duì)于ViewTarget及其子類來說,會(huì)注冊(cè)View的onPreDrawListener事件权烧,等待View初始化完成后調(diào)用SingleRequest#onSizeReady方法疫向,這個(gè)方法里就是加載圖片的入口。
添加監(jiān)聽的路徑:
在SingleRequest#begin里:
初始化完成回調(diào):
所以要看圖片怎么加載的豪嚎,還是要看onSizeReady方法里怎么處理的:
View初始化完成后,調(diào)用Engine#load方法加載圖片:
Engine#load方法中會(huì)根據(jù)寬高谈火、簽名侈询、request options等參數(shù)構(gòu)建EngineKey,并以此為鍵去緩存中查找是否有文件緩存糯耍,調(diào)用的是#loadFromMemory方法扔字。如果沒有緩存,則使用現(xiàn)存或者開啟新的任務(wù)來加載圖片温技。
我們接下里分析一下Engine#loadFromMemory革为,也就是Glide的緩存處理:
可以看到這里有兩級(jí)緩存,先是從#loadFromActiveResources中獲取EngineResource(ActiveResource中有個(gè)HashMap舵鳞,value是我們圖片的key震檩,value是ResourceWeakRefenerce,ResourceWeakReference中持有了我們的圖片的Resource對(duì)象)。
如果支持內(nèi)存緩存則存入緩存蜓堕,否則為null抛虏。
如果不為空,則直接返回套才;若為空迂猴,再調(diào)用#loadFromCache獲取磁盤緩存返回;若為空背伴,則調(diào)用#waitForExistingOrStartNewJob通過網(wǎng)絡(luò)去加載圖片沸毁。當(dāng)圖片加載完成,調(diào)用onResourceReady方法分發(fā)給各個(gè)Target去設(shè)置圖片傻寂。然后調(diào)用notifyLoadSuccess方法通知ThumbnailRequestCoordinator圖片加載成功息尺。
**Glide磁盤緩存策略分為四種,默認(rèn)的是RESULT(默認(rèn)值這一點(diǎn)網(wǎng)上很多文章都寫錯(cuò)了,但是這一點(diǎn)很重要):
1.ALL:緩存原圖(SOURCE)和處理圖(RESULT)
2.NONE:什么都不緩存
3.SOURCE:只緩存原圖(SOURCE)
4.RESULT:只緩存處理圖(RESULT) —默認(rèn)值**
我們來仔細(xì)分析一下【怎么通過網(wǎng)絡(luò)進(jìn)行圖片加載的】:
#waitForExistingOrStartNewJob方法中,會(huì)封裝一個(gè)DecodeJob疾掰,將DecodeJob放到EngineJob持有的GlideExecutor也就是Glide加載圖片的線程池中運(yùn)行掷倔。(其中DecodeJob是解碼資源和轉(zhuǎn)換的類,EngineJob是加載調(diào)度類个绍,主要就是處理根中回調(diào))DecodeJob實(shí)現(xiàn)了Runnable接口勒葱,那么我們就可以去其#run()方法中查看了:
拿到DataFetcher對(duì)象浪汪,調(diào)用runWrapped方法-->runGenerators()-->DataFetcherGenerator#startNext()--->SourceGenerator#startNextLoad():
最后調(diào)用到DataFetcher#loadData()方法
DataFetcher是一個(gè)接口,負(fù)責(zé)數(shù)據(jù)加載凛虽。其實(shí)現(xiàn)類如下:
其網(wǎng)絡(luò)加載的實(shí)現(xiàn)類锌杀,基本上就可以確定是HttpUrlFetcher.
果不其然,看一下其#loadData方法:
調(diào)用#loadDataWithRedirects方法獲取輸入流嫉沽,并調(diào)用
DataCallback#onDataReady回調(diào)昔逗,如果IO異常,調(diào)用onLoadFailed方法至非。
在loadDataWithRedirects方法中建立HttpURLConnection加載圖片:
這就是Glide加載網(wǎng)絡(luò)圖片的簡(jiǎn)單分析钠署!
2.ThumbnailRequestCoordinator
這個(gè)類就是協(xié)調(diào)兩個(gè)請(qǐng)求,因?yàn)橛械恼?qǐng)求需要同時(shí)加載原圖和縮略圖荒椭,比如啟動(dòng)這兩個(gè)請(qǐng)求谐鼎,原圖加載完成后縮略圖就不用等待了等等,這些控制都由這個(gè)類控制趣惠。
3.ErrorRequestCoordinator
當(dāng)加載失敗時(shí)可能希望通過網(wǎng)絡(luò)或者本地資源加載另一張錯(cuò)誤占位圖狸棍,就是通過此類協(xié)調(diào)ThumbnailRequestCoordinator以及error中的Request。
Target(Glide中很重要的概念味悄!)
Target代表一個(gè)可被Glide加載并具有生命周期的資源草戈。當(dāng)我們調(diào)用RequestBuilder#into方法時(shí),會(huì)根據(jù)傳入?yún)?shù)創(chuàng)建對(duì)應(yīng)類型的Target實(shí)現(xiàn)類侍瑟。
其角色就是指加載完成的圖片應(yīng)該放在哪唐片。是setImageDrawable還是setBackgroudDrawable等。
Target的實(shí)現(xiàn)很多:
我們這里只看幾個(gè)常用到的:
1.CustomViewTarget & ViewTarget
抽象類涨颜,負(fù)責(zé)加載Bitmap牵触、Drawable并且放到View上。
ViewTarget:所有View相關(guān)的Target都是繼承ViewTarget咐低,但是已經(jīng)被標(biāo)記為過期類揽思,推薦將ViewTarget替換成CustomViewTarget。
2.ImageViewTarget
是加載到ImageView上的Target见擦,繼承自ViewTarget钉汗,同樣也是個(gè)抽象類。
構(gòu)造器中限定了必須傳入ImageView或者其子類鲤屡,圖片數(shù)據(jù)加載完成后會(huì)回調(diào)其中的onResourceReady方法损痰,第一步是將圖片設(shè)置給ImageView,第二步是判斷是否需要使用動(dòng)畫酒来,需要的話就執(zhí)行動(dòng)畫卢未。
雖然目前有5個(gè)子類,但主要用于區(qū)分加載的資源是Bitmap還是Drawable類型。
3.RequestFutureTarget
用來同步加載圖片的Target辽社,調(diào)用RequestBuilder#sumbit將會(huì)返回一個(gè)FutureTarget伟墙,調(diào)用get方法即可獲取到加載的資源對(duì)象。
4.AppWidgetTarget
用于將下載的 Bitmap 設(shè)置到 RemoteView 上滴铅。
其他的暫且不分析了戳葵。
還有幾個(gè)重要的概念需要知道:
1.DataFetcher
是一個(gè)接口,最重要的方法還loadData汉匙,也就是加載數(shù)據(jù)
內(nèi)部是通過HttpUrlConnection發(fā)起網(wǎng)絡(luò)請(qǐng)求拱烁、打開文件或者使用AssetManager打開一個(gè)資源等等。
加載完成后通過DataFetcher$DataCallback接口回調(diào)噩翠。
此接口有兩個(gè)方法
分別代表數(shù)據(jù)加載成功或者加載失敗回調(diào)戏自。
2.Encoder
也是一個(gè)接口。用來將給定的數(shù)據(jù)寫入文件中
如注釋所寫伤锚,就是把data存入文件中擅笔。
數(shù)據(jù)加載完成后會(huì)先使用Encoder將數(shù)據(jù)存入本地磁盤緩存文件中。
緩存目錄见芹,我查看的,可能是下面這個(gè):
Encoder對(duì)應(yīng)的實(shí)現(xiàn)類是在Glide初始化時(shí)注冊(cè)進(jìn)去的蠢涝。
3.ResourceDecoder
與Encoder對(duì)應(yīng)玄呛,數(shù)據(jù)解碼器,用來將原始數(shù)據(jù)解碼成相應(yīng)的數(shù)據(jù)類型和二。
針對(duì)不同的請(qǐng)求實(shí)現(xiàn)類都不同徘铝,例如通過網(wǎng)絡(luò)請(qǐng)求最終獲取到的是一個(gè) InputStream,經(jīng)過 ByteBufferBitmapDecoder 解碼后再生成一個(gè) Bitmap惯吕。
需要指出的是惕它,這里解碼時(shí)會(huì)根絕 option 以及圖片大小(如果有的話)按需加載 Bitmap废登,防止內(nèi)存的浪費(fèi)淹魄。【按需加載在這里1ぞ唷<孜!】
與 Encoder 一樣羽戒,Glide 初始化時(shí)會(huì)注冊(cè)很多個(gè)類型的 ResourceDecoder 實(shí)現(xiàn)類缤沦,圖片數(shù)據(jù)獲取到之后會(huì)根據(jù)不同的類型使用對(duì)應(yīng)的解碼器對(duì)其解碼。
4.Engine
執(zhí)行引擎易稠,算是整個(gè) Glide 的核心發(fā)動(dòng)機(jī)缸废。
Engine 負(fù)責(zé)管理請(qǐng)求以及活動(dòng)資源、緩存等。主要關(guān)注 load 方法企量,這個(gè)方法主要做了如下幾件事:
通過請(qǐng)求構(gòu)建 Key测萎;
從活動(dòng)資源中獲取資源(詳見緩存章節(jié)),獲取到則返回梁钾;
從緩存中獲取資源绳泉,獲取到則直接返回;
判斷當(dāng)前請(qǐng)求是否正在執(zhí)行姆泻,是則直接返回零酪;
構(gòu)建 EngineJob 與 DecodeJob 并執(zhí)行。
Glide生命周期管理
Glide Bitmap復(fù)用機(jī)制
【Glide生命周期管理】
Glide的生命周期處理邏輯由RequestManagerRetriever#get方法出發(fā):
此方法有五個(gè)重載方法:
我們主要分析傳入FragmentActivity參數(shù)的#get方法
1.如果當(dāng)前是在子線程中就是用Application的context拇勃,也就是生命周期和Application保持一致四苇。
2.如果不是在子線程中,則先判斷activity是否銷毀方咆。拿到當(dāng)前Activity的FragmentManager月腋,通過#supportFragmentGet方法創(chuàng)建一個(gè)Fragment。
可以看到調(diào)用RequestManagerRetriever#getSupportRequestManagerFragment方法創(chuàng)建一個(gè)Fragment.
3.從SupportRequestManagerFragment中獲取RequestManager,如果為空瓣赂,則直接創(chuàng)建RequestManager并綁定此Fragment上榆骚。
創(chuàng)建RequestManager對(duì)象過程中,會(huì)傳入ActivityFragmentLifecycle煌集。這樣RequestManager也就是我們的請(qǐng)求管理類中也有了監(jiān)控生命周期的能力妓肢。
如圖一所標(biāo)。
我們看一下RequestManager是怎么通過RequestManagerFactory#build創(chuàng)建的
通過默認(rèn)的RequestManagerFactory: DEFAULT_FACTORY的build方法中直接new了一個(gè)RequestManager苫纤〉锬疲【這個(gè)寫法蠻有意思的,之前沒這么寫過卷拘,抄喊废,必須抄】
看一下RequestManager構(gòu)造方法里的邏輯處理:
將當(dāng)前對(duì)象注冊(cè)到ActivityFragmentLifecycle中,如果是子線程則通過Handler將當(dāng)前對(duì)象注冊(cè)到ActivityFragmentLifecycle中栗弟。
同時(shí)添加網(wǎng)絡(luò)監(jiān)聽污筷。
自然RequestManager實(shí)現(xiàn)了LifecycleListener接口,并實(shí)現(xiàn)了接口中的方法乍赫。Fragment生命周期變化時(shí)會(huì)主動(dòng)通知lifecycle執(zhí)行相關(guān)方法颓屑。
可以看到在onStart、onStop和onDestory生命周期中耿焊,RequestManager會(huì)對(duì)請(qǐng)求進(jìn)行相應(yīng)處理揪惦。(resumeRequest、pauseRequest等)
從上面幾點(diǎn)的闡述可以看出罗侯,構(gòu)造RequestManager的時(shí)候就將RequestManager的生命周期與Fragment關(guān)聯(lián)起來了器腋。
下面就是【簡(jiǎn)單的總結(jié)】:
Glide生命周期回調(diào)流程:
Activity/Fragment->RequestManagerFragment->ActivityFragmentLifecycle->RequestManager->根據(jù)生命周期變化做業(yè)務(wù)處理
?** Glide.with(this)綁定了Activity的生命周期。在Activity內(nèi)新建了一個(gè)無UI的Fragment,這個(gè)Fragment持有一個(gè)Lifecycle纫塌,通過Lifecycle在Fragment關(guān)鍵生命周期通知RequestManager進(jìn)行相關(guān)從操作诊县。在生命周期onStart時(shí)繼續(xù)加載,onStop時(shí)暫停加載措左,onDestory時(shí)停止加載任務(wù)和清除操作依痊。**
【靈魂發(fā)問】Glide為什么要新建一個(gè)無界面的Fragment來管理生命周期呢?
參考資料
圖片的一般是異步的怎披,異步經(jīng)常面臨的問題是內(nèi)存泄露和異步加載回來view已經(jīng)銷毀導(dǎo)致的空指針問題胸嘁。而Glide在使用的時(shí)候只要求傳入當(dāng)前加載的view或者context,且沒有用setLifeCyclelistener什么的方法就實(shí)現(xiàn)了生命周期的管理凉逛。這就是Glide為什么要是用空白Fragment來管理生命周期了性宏,這樣我們上層使用的時(shí)候就不需要自己來管理圖片加載的生命周期了。
空白Fragment的其他妙用就是:
動(dòng)態(tài)申請(qǐng)權(quán)限状飞,對(duì)于不是必要的權(quán)限不同意就退出app體驗(yàn)會(huì)很糟糕毫胜,我們就就可以將這些權(quán)限放在空白Fragment中,哪個(gè)地方需要申請(qǐng)就持有當(dāng)前Fragment即可诬辈。
BitmapPool(Glide是如何實(shí)現(xiàn)Bitmap復(fù)用的酵使?)
BitmapPool分析
首先明白兩個(gè)概念:
池化思想
核心是復(fù)用
復(fù)用相同的資源,減少浪費(fèi)焙糟,減少新建和銷毀的成本口渔;
減少單獨(dú)管理的成本,統(tǒng)一交由"池"酬荞;
集中管理搓劫,減少"碎片"瞧哟;
提高系統(tǒng)響應(yīng)速度混巧,因?yàn)槌刂杏鞋F(xiàn)成的資源,不用重新去創(chuàng)建勤揩;
池化思想在Android中的運(yùn)用:
1.線程池
2.Handler Message池
3.Java內(nèi)存池
inBitmap參數(shù)
在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段咧党。如果設(shè)置了此選項(xiàng),那么采用 Options 對(duì)象的解碼方法會(huì)在加載內(nèi)容時(shí)嘗試重復(fù)使用現(xiàn)有位圖陨亡。這樣可以復(fù)用現(xiàn)有的 Bitmap傍衡,減少對(duì)象創(chuàng)建,從而減少發(fā)生 GC 的概率负蠕。不過蛙埂,inBitmap 的使用方式存在某些限制。特別是在 Android 4.4(API 級(jí)別 19)之前遮糖,系統(tǒng)僅支持大小相同的位圖绣的。在 Android 4.4 之后的版本,只要內(nèi)存大小不小于需求的 Bitmap 都可以復(fù)用。
在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段屡江。如果設(shè)置了此選項(xiàng)芭概,那么采用 Options 對(duì)象的解碼方法會(huì)在加載內(nèi)容時(shí)嘗試重復(fù)使用現(xiàn)有位圖。這樣可以復(fù)用現(xiàn)有的 Bitmap惩嘉,減少對(duì)象創(chuàng)建罢洲,從而減少發(fā)生 GC 的概率。不過文黎,inBitmap 的使用方式存在某些限制惹苗。特別是在 Android 4.4(API 級(jí)別 19)之前,系統(tǒng)僅支持大小相同的位圖臊诊。在 Android 4.4 之后的版本鸽粉,只要內(nèi)存大小不小于需求的 Bitmap 都可以復(fù)用。
BitmapPool是一個(gè)接口抓艳,有兩個(gè)實(shí)現(xiàn)類:
1.BitmapPoolAdapter
2.LruBitmapPool
默認(rèn)實(shí)現(xiàn)是LruBitmapPool触机。inBitmap 以 Android 4.4 為分水嶺,之前和之后的版本在使用上存在版本差異玷或,那么 BitmapPool 是如何處理這個(gè)差異的呢儡首?答案是策略模式。Glide 定義了 LruPoolStrategy 接口偏友,該接口內(nèi)部定義了增刪相關(guān)操作蔬胯。真實(shí)的 Bitmap 數(shù)據(jù)根據(jù)尺寸和顏色等映射關(guān)系存儲(chǔ)到 LruPoolStrategy 中。BitmapPool 的 get 和 put 也是通過 LruPoolStrategy 的 get 和 put 完成的位他。
LruPoolStrategy 默認(rèn)提供了三個(gè)實(shí)現(xiàn)氛濒,分別是 AttributeStrategy、SizeConfigStrategy 和 SizeStrategy. 其中鹅髓,AttributeStrategy 適用于 Android 4.4 以下的版本舞竿,SizeConfigStrategy 和 SizeStrategy 適用于 Android 4.4 及以上的版本。
AttributeStrategy 通過 Bitmap 的 width(圖片寬度)窿冯、height(圖片高度) 和 config(圖片顏色空間骗奖,比如 ARGB_8888 等) 三個(gè)參數(shù)作為 Bitmap 的唯一標(biāo)識(shí)。當(dāng)獲取 Bitmap 的時(shí)候只有這三個(gè)條件完全匹配才行醒串。而 SizeConfigStrategy 使用 size(圖片的像素總數(shù)) 和 config 作為唯一標(biāo)識(shí)执桌。當(dāng)獲取的時(shí)候會(huì)先找出 cofig 匹配的 Bitmap(一般就是 config 相同),然后保證該 Bitmap 的 size 大于我們期望的 size 并且小于期望 size 的 8 倍即可復(fù)用(可能是為了節(jié)省內(nèi)存空間)芜赌。
所謂的 LRU 就是 BitmapPool 通過 LruPoolStrategy 實(shí)現(xiàn)的仰挣,具體操作是,在往 BitmapPool 中 put 數(shù)據(jù)之后會(huì)執(zhí)行下面的操作調(diào)整空間大小
回顧一下Bitmap加載的一般流程:
// 設(shè)置 inJustDecodeBounds 為 true 來獲取圖片尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 設(shè)置 inJustDecodeBounds 為 false 來真正加載
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
也就是說缠沈,首先通過設(shè)置 options.inJustDecodeBounds 為 true 來獲取圖片真實(shí)的尺寸膘壶,以便設(shè)置采樣率违柏。因?yàn)槲覀円话悴粫?huì)直接加載圖片的所有的像素,而是采樣之后再按需加載香椎,以減少圖片的內(nèi)存占用漱竖。當(dāng)真正需要加載的時(shí)候,設(shè)置 options.inJustDecodeBounds 為 false畜伐,再調(diào)用 decode 相關(guān)的方法即可馍惹。
那么 Bitmap 復(fù)用是如何使用的呢?很簡(jiǎn)單玛界,只需要在加載的時(shí)候通過 options 的 inBitmap 參數(shù)指定一個(gè) Bitmap 對(duì)象再 decode 即可:
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
Glide如何加載Bitmap
Glide在初始化的時(shí)候万矾,在GlideBuilder#build中會(huì)創(chuàng)建BitmapPool,在Glide構(gòu)造方法中慎框,會(huì)創(chuàng)建Downsampler良狈。Glide的Bitmap加載流程就位于Downsampler類中。
從其他渠道笨枯,比如網(wǎng)絡(luò)或者磁盤中獲取到一個(gè)輸入流 InputStream 之后就可以進(jìn)行圖片加載了薪丁。執(zhí)行流程在其#decodeFromWrappedStreams方法中:
首先通過設(shè)置inJustDecodeBounds讀取圖片的原始尺寸信息
根據(jù)要求計(jì)算需要記載的圖片大小和config,計(jì)算結(jié)果直接設(shè)置給bitmap的options
根據(jù)圖片的期望尺寸到BitmapPool獲取一個(gè)Bitmap以復(fù)用馅精。
然后執(zhí)行decodeStream邏輯严嗜,獲取到我們的目標(biāo)Bitmap。
Bitmap池復(fù)用邏輯如上所述洲敢。
Glide 首先會(huì)通過設(shè)置 inBitmap 復(fù)用的方式加載圖片漫玄。如果這個(gè)過程中出現(xiàn)了異常孝鹊,因?yàn)榇藭r(shí) inBitmap 不為空均蜜,所以將會(huì)進(jìn)入異常處理流程铣卡,此時(shí)會(huì)清理掉 inBitmap驻仅,再次調(diào)用 decodeStream 方法二次加載,這個(gè)時(shí)候就不是 Bitmap 復(fù)用的了蔼夜。所以犬金,Glide 內(nèi)部會(huì)通過錯(cuò)誤重試機(jī)制進(jìn)行 Bitmap 復(fù)用份蝴,當(dāng)復(fù)用并出現(xiàn)錯(cuò)誤的時(shí)候忆畅,會(huì)降級(jí)為非復(fù)用的方式第二次進(jìn)行加載衡未。
為什么要進(jìn)行三級(jí)緩存尸执?
三級(jí)緩存策略家凯,最實(shí)在的意義就是減少不必要的流量消耗,增加加載速度如失。
Bitmap 的創(chuàng)建非常消耗時(shí)間和內(nèi)存绊诲,可能導(dǎo)致頻繁GC。而使用緩存策略褪贵,會(huì)更加高效地加載 Bitmap掂之,減少卡頓抗俄,從而減少讀取時(shí)間。
而內(nèi)存緩存的主要作用是防止應(yīng)用重復(fù)將圖片數(shù)據(jù)讀取到內(nèi)存當(dāng)中世舰,硬盤緩存則是防止應(yīng)用重復(fù)從網(wǎng)絡(luò)或其他地方重復(fù)下載和讀取數(shù)據(jù)动雹。
Glide是如何清理緩存的
Glide實(shí)現(xiàn)了ComponentCallbacks2接口,實(shí)現(xiàn)了onTrimMemory方法跟压。此方法會(huì)在系統(tǒng)回收不需要的內(nèi)存時(shí)調(diào)用:
其中調(diào)用了Glide的trimeMomery方法胰蝠,此方法中會(huì)清理memoryCache、bitmapPool震蒋、arrayPool茸塞。
memoryCache會(huì)調(diào)用LruResourceCache的trimMemory,最終調(diào)用LruCache的trimToSize方法清除內(nèi)存緩存查剖。
LruCache原理解析
參考博客一
參考博客二
Lru -->Least Recently Used钾虐,最近最少使用.
LruCache是個(gè)泛型類,主要算法原理是把最近使用的對(duì)象用強(qiáng)引用(即我們平常使用的對(duì)象引用方式)存儲(chǔ)在 LinkedHashMap 中笋庄。當(dāng)緩存滿時(shí)效扫,把最近最少使用的對(duì)象從內(nèi)存中移除,并提供了get和put方法來完成緩存的獲取和添加操作直砂。
LruCache里最重要的就是使用LinkedHashMap存儲(chǔ)緩存數(shù)據(jù)荡短,為什么使用LinkedHashMap,這個(gè)我們后面會(huì)說哆键。
除了這個(gè)map掘托,還有兩個(gè)參數(shù)比較重要:
size:當(dāng)前緩存已使用大小。
maxSize:LruCache能使用的內(nèi)存的最大值籍嘹。
先分析其get和put方法:
1.get()
通過key獲取緩存的數(shù)據(jù)闪盔,如果通過這個(gè)方法得到了需要的元素,那么這個(gè)元素會(huì)被放到緩存隊(duì)列的頭部辱士±嵯疲可以理解成最近常用的元素,不會(huì)在緩存空間不足時(shí)被清理掉颂碘。
這里加了synchronized异赫,同步代碼塊,所以LruCache的get是線程安全的头岔。
如果通過key從緩存結(jié)合中獲取不到緩存數(shù)據(jù)塔拳,就嘗試使用create方法創(chuàng)造一個(gè)新數(shù)據(jù)并放入緩存中。當(dāng)然需要我們重寫create(key)方法峡竣,不重寫默認(rèn)為null靠抑。
2.put()
將創(chuàng)建的新元素添加到緩存隊(duì)列,并添加成功后返回這個(gè)元素适掰。 在同步代碼塊中size就是對(duì)象占用內(nèi)存的大小(所有對(duì)象占用內(nèi)存總和颂碧,所以是+=)荠列,所以我們?cè)诰彺鎴D片時(shí)需要重寫sizeOf方法,如果添加失敗size將會(huì)減去圖片內(nèi)存大小载城。
因?yàn)榧恿藄ynchronized肌似,所以可以看出put也是線程安全的操作。
最后調(diào)用了trimToSize方法诉瓦,修改緩存大小锈嫩,使已經(jīng)使用的緩存(size)不大于設(shè)置的緩存最大值(maxSize)
接下來我們說一下最重要的點(diǎn):
【為什么使用LinkedHashMap進(jìn)行緩存】?
參考博客
這個(gè)跟算法有關(guān)垦搬,LinkedHashMap剛好能提供LruCache所需要的最近最少使用算法呼寸。
LinkedHashMap內(nèi)部本來就有個(gè)排序功能,當(dāng)?shù)谌齻€(gè)參數(shù)是true的時(shí)候猴贰,數(shù)據(jù)在被訪問的時(shí)候就會(huì)排序对雪,這個(gè)排序的結(jié)果就是把最近訪問的數(shù)據(jù)放到集合的最后面。
我們?cè)贚ruCache的構(gòu)造參數(shù)中new LinkedHashMap的時(shí)候米绕,第三個(gè)參數(shù)accessOrder就是傳的true瑟捣。
LinkedHashMapEntry的定義
LinkedHashMap內(nèi)部是使用雙向循環(huán)鏈表來存儲(chǔ)數(shù)據(jù)的。也就是每一個(gè)元素都持有它上一個(gè)元素地址和下一個(gè)元素地址栅干,元素實(shí)體類就是LinkedHashMapEntry迈套。
其定義如下:
當(dāng)集合的get方法被調(diào)用時(shí),會(huì)調(diào)用afterNodeAccess方法(jdk中是recordAccess)碱鳞。如果accessOrder為true桑李,就把這個(gè)元素放在集合的最末端。
LinkedHashMap#get()方法內(nèi)容很簡(jiǎn)單:
就是判斷accessOrder是否為true窿给,如果為true贵白,調(diào)用afterNodeAccess,然后返回節(jié)點(diǎn)Node的value崩泡。
排序過程:
①當(dāng)LinkedHashMap初始化的時(shí)候會(huì)初始化一個(gè)頭結(jié)點(diǎn)head;
這個(gè)頭結(jié)點(diǎn)的前節(jié)點(diǎn)和后節(jié)點(diǎn)都指向自己禁荒。
②當(dāng)獲取一個(gè)節(jié)點(diǎn)的時(shí)候,會(huì)拿到的當(dāng)前節(jié)點(diǎn)p的前節(jié)點(diǎn)b和后節(jié)點(diǎn)a角撞,
首先將后節(jié)點(diǎn)置為null呛伴, 將后節(jié)點(diǎn)a的前節(jié)點(diǎn)指定為b;接著拿到最后一個(gè)節(jié)點(diǎn)tail谒所,將當(dāng)前節(jié)點(diǎn)p指定為tail的前節(jié)點(diǎn)热康,這樣當(dāng)前節(jié)點(diǎn)就為鏈表的最后一個(gè)元素。
這樣就完成了一次Lru排序百炬。
將最近訪問的數(shù)據(jù)放在了鏈表的結(jié)尾褐隆,鏈表越靠前的越不常用污它,緩存空間不夠就優(yōu)先清除前面的剖踊。
LinkedHashMap還有一個(gè)方法eldest庶弃,返回最近最少使用的元素:
返回的就是鏈表的頭結(jié)點(diǎn)head。
#timeToSize
這是LruCache核心方法之一了德澈,get和put都可能會(huì)執(zhí)行此方法歇攻。
這個(gè)方法會(huì)檢查已用的緩存大小和設(shè)置的最大緩存大小。當(dāng)發(fā)現(xiàn)需要進(jìn)行刪除數(shù)據(jù)來騰出緩存空間的時(shí)候梆造,會(huì)調(diào)用LinkedHashMap的eldest()方法來刪除頭結(jié)點(diǎn)缴守,也就是最近最少使用的節(jié)點(diǎn)。
這就是LruCache的大致工作原理镇辉。
手寫三級(jí)緩存
19年寫過屡穗,現(xiàn)在是21年5月10號(hào),我就直接截圖了:
這里L(fēng)ruCache大小為運(yùn)行緩存的八分之一忽肛。