android點(diǎn)二

一舔示、編譯時(shí)與運(yùn)行時(shí)

編譯時(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í)候,可以看到:

image.png

image.png

image.png

可以看到在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ì)象。

image.png

Sync是ReentrantLock中的一個(gè)抽象靜態(tài)內(nèi)部類志衍,其繼承自AbstractQueueSynchronizer.這個(gè)AbstractQueueSynchronizer實(shí)際上就是我們常說的AQS
我們先看一下NonfairSync和FairSync里公平鎖與非公平鎖如何實(shí)現(xiàn)的:
①NonfairSync
image.png

image.png

image.png

可以看到NonfairSync中并不會(huì)判斷是否有阻塞隊(duì)列的存在暖庄,而是直接調(diào)用UnSafe里的compareAndSwap(native層實(shí)現(xiàn))也就是CAS去獲取鎖。
②FairSync

image.png

image.png

而FairSync會(huì)判斷當(dāng)前線程是否位于同步隊(duì)列的首位楼肪,是返回true培廓,如果通過CAS也能獲取到鎖,則當(dāng)前線程拿到當(dāng)前鎖

公平鎖與非公平鎖 中鎖的獲取過程

我們?cè)偻锟纯垂芥i和非公平鎖是如何給當(dāng)前線程加鎖的淹辞,先看一下非公平鎖的加鎖過程:
非公平鎖獲取鎖過程

image.png

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方法:
image.png

image.png

image.png

這時(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:

image.png

image.png

創(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)建的:


image.png

image.png

image.png

image.png

可以看到這四個(gè)線程池底層都是ThreadPoolExecutors創(chuàng)建的柑船。ThreadPoolExecutors構(gòu)造參數(shù),有七個(gè)泼各。如下:


image.png

image.png

①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:

image.png

這種情況下沉填,一旦提交的線程數(shù)超過當(dāng)前可用線程數(shù)時(shí),就會(huì)拋出java.untl.concurrent.RejectdExecutionException.異常(Exeception)總比錯(cuò)誤(Error)好佑笋。
說到錯(cuò)誤和異常翼闹,我們下面簡(jiǎn)單說一下程序中捕獲異常和錯(cuò)誤的問題。

java exception和 error

java 中error可不可以捕獲蒋纬?答案是可以的猎荠。

image.png

image.png

可以看到error和exception都繼承自Throwable坚弱,我們完全可以在catch語句中捕獲Throwable,也就是error可以捕獲
image.png

那為什么不該捕獲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方法,沒有返回值葬项,不能拋出異常泞当。

image.png

②Callable
參考資料
Callable也是一個(gè)接口,只有一個(gè)call方法民珍。和Runnable差別在于它有返回的結(jié)果襟士,而且可以拋出異常!一般配合ThreadPoolExecutor使用嚷量。[??這是真的嗎]
image.png

Callable確實(shí)主要在線程池中使用,如下圖,在ThreadPoolExecutor的父類AbstractExecutorService中有引用到Callable:
image.png

③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)前的線程。
image.png

使用demo:
image.png

④FutureTask
因?yàn)镕uture只是一個(gè)接口傻粘,所以無法創(chuàng)建使用每窖,因此有了FutureTask.
image.png

image.png

FutureTask相當(dāng)于繼承了Runnable和Future帮掉。
因此它可以作為Runnable被線程執(zhí)行,又可以有Future的那些操作岛请。
Demo:
image.png

as we all know旭寿,線程池執(zhí)行任務(wù)有兩種方法,一是execute崇败,而是submit盅称。
如果們需要返回任務(wù)的執(zhí)行結(jié)果就得調(diào)用submit方法而不是execute。
submit也不神秘后室,就是將任務(wù)封裝成FutureTask再execute缩膝。
image.png

image.png

所以submit三個(gè)方法其實(shí)都是把task轉(zhuǎn)成FutureTask,如果task是Callable岸霹,就直接賦值疾层。如果是Runnable就轉(zhuǎn)為Callable再賦值,只不過返回值是null贡避。
image.png

image.png

image.png

此外痛黎,sumbit有個(gè)方法:
image.png

demo:
image.png

image.png

傳入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é)果的方法撮弧。

線程池方法源碼解析

參考資源一
參考資源二
①shutdown()

image.png

當(dāng)線程池調(diào)用該方法時(shí),線程池的狀態(tài)通過CAS會(huì)變成SHUTDOWN狀態(tài)姚糊。此時(shí),不能再往線程池中添加任務(wù)授舟,否則會(huì)拋出RejectedExecutionException救恨。

image.png

image.png

有幾個(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。如下圖:

image.png

image.png

3>為什么對(duì)運(yùn)行中的任務(wù)不產(chǎn)生任何影響寂纪?
在調(diào)用中斷任務(wù)的方法時(shí)席吴,tryTerminate方法會(huì)檢測(cè)workers中的線程,如果沒有中斷捞蛋,并且是空閑線程孝冒,才會(huì)去中斷這個(gè)線程。
image.png

image.png

這里判斷空閑線程的方法也很簡(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
image.png

從最上層的調(diào)用可以看到糕篇,與shutdown不同的是通過CAS將狀態(tài)改成STOP,shutdown是改成了SHUTDOWN.在組織新來的任務(wù)提交的同時(shí),會(huì)中斷當(dāng)前正在運(yùn)行的線程该窗,及workes中的線程弟蚀。另外將workQueue中的任務(wù)給移除,并將這些任務(wù)添加到列表中酗失。
image.png

image.png

image.png

可以看到在interruptWorkers方法中义钉,會(huì)加鎖判斷所有的Worker是否已經(jīng)運(yùn)行且沒有被中斷,如果滿足上述條件规肴,則調(diào)用interrupt方法中斷所有運(yùn)行的線程捶闸。
image.png

image.png

接著調(diào)用drainQueue,將阻塞隊(duì)列也就是workQueue(BlockingQueue)中的任務(wù)給移除掉拖刃,并將這些任務(wù)添加到列表中返回删壮。
然后調(diào)用tryTerminate,將空閑線程也給中斷掉兑牡,CAS判斷后終止線程池央碟。
image.png

相關(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ì)是怎樣的岖是?

image.png

①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í)行部分爷肝。
image.png

②ExecutorService接口增加了一些能力:擴(kuò)充執(zhí)行任務(wù)的的能力,補(bǔ)充可以為一個(gè)活一批任務(wù)生成Future的方法;提供了管控線程池的方法灯抛,比如停止線程池的運(yùn)行(shutDown/shutDownNow/isShutDown)
image.png

③AbstractExecutorService則是上層的抽象類金赦,將執(zhí)行任務(wù)的流程串聯(lián)起來,保證下層的實(shí)現(xiàn)只需關(guān)注一個(gè)執(zhí)行任務(wù)的方法接口对嚼。實(shí)現(xiàn)了submit相關(guān)方法夹抗。
image.png

④最下層的實(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:

image.png

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)下面這種情況:
image.png

阻塞與非阻塞

delay()是非阻塞的捺宗;
Thread.sleep()是阻塞的柱蟀;
runBlocking{}是阻塞的:
調(diào)用runBlocking的主線程會(huì)一直阻塞直到runBlocking內(nèi)部的協(xié)程執(zhí)行完畢。
runBlocking是一個(gè)全局函數(shù)蚜厉,可在任意地方調(diào)用长已,不過 項(xiàng)目中用得不多,畢竟堵塞main線程意義不大昼牛,常用于單元測(cè)試防止JVM退出术瓮。


image.png

協(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é)程的上下文:


image.png

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
image.png

可以看到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)存泄露军洼。

image.png

②使用MainScope()函數(shù)
為了在Android場(chǎng)景中更方便的使用,官方提供了MainScope()函數(shù)快速創(chuàng)建基于主線程協(xié)程作用域演怎。
如下圖(并不能運(yùn)行哈匕争,只是一個(gè)示例,沒有上下文環(huán)境)
image.png

③使用coroutineScope()和supervisorScope()創(chuàng)建子作用域
image.png

作用域函數(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í)返回值咆耿。
如下:

image.png

掛起函數(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)浸策。

image.png

如上圖冯键,如果不加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)異常

image.png

無法使用try-catch捕獲launch和async作用域的異常
2.全局異常處理(throw)
image.png

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

image.png

啟動(dòng)模式

launch&async第二個(gè)參數(shù)CoroutineStart,可以指定協(xié)程的啟動(dòng)模式:


image.png

image.png

協(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é)果的情況。
    image.png

五暂衡、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。

image.png

②當(dāng)我們調(diào)用get()方法時(shí)冲九,先獲取當(dāng)前線程谤草,然后獲取當(dāng)前線程的ThreadLocalMap對(duì)象。如果非空就取出ThreadLocal的value莺奸,否則進(jìn)行初始化丑孩,初始化就是將initialValue的值set到ThreadLocal中壹瘟。
image.png

③當(dāng)我們調(diào)用set()方法時(shí)纫事,就是拿到當(dāng)前的線程對(duì)應(yīng)的ThreadLocal霉颠,不為空碟摆,賦值明场。為空,創(chuàng)建ThreadLocalMap綁定Thread并賦值慰丛。
ThreadLocalMap是以弱引用的ThreadLocal為key的擎析,不是Thread!
image.png

⑤總結(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)

image.png

可以看到拗踢,Glide采用五層架構(gòu)設(shè)計(jì)脚牍,從高到低依次是:
Request-->Engine-->Get Data-->Data-->Resource
按照邏輯功能劃分可以分為以下幾種:
image.png

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è)重載方法;

image.png

如源碼注釋所說叉庐,RequestManager是Glide的請(qǐng)求管理類。
我們根據(jù)最上層的調(diào)用分析源碼:
image.png

1.with方法

image.png

image.png

首先判斷是否在主線程中調(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)
image.png

image.png

image.png

在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)建的:

image.png

image.png

這個(gè)GlideBuilder#build方法是Glide創(chuàng)建的核心方法梧田,我們來仔細(xì)分析一下build方法:
image.png

image.png

image.png

可以看到build中首先新建了三個(gè)線程池
1.sourceExecutor:負(fù)責(zé)處理內(nèi)存緩存(淳蔼?侧蘸?)
image.png

2.diskCacheExecutor:負(fù)責(zé)處理磁盤緩存
3.animationExecutor:負(fù)責(zé)加載Gif動(dòng)圖的每一幀圖像
這三個(gè)線程池底層都是GlideExecutor的build方法中通過ThreadPoolExecutor創(chuàng)建的 corePoolSize和maximumPoolSize都不相同:
image.png

接著創(chuàng)建了Bitmap池,也就是LruBitmapPool和BitmapPoolAdapter鹉梨;
以及數(shù)組池:LruArrayPool讳癌。
重頭戲來了,創(chuàng)建的memoryCache就是面試中常被問的緩存策略存皂。
image.png

image.png

可以看到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疤孕、縮略圖商乎、加載失敗占位圖等。

image.png

image.png

image.png

這里返回的RequestBuilder使用的是克隆模式祭阀,用了一次深拷貝鹉戚。
深拷貝與淺拷貝的區(qū)別

3.into()方法

最重要的就是這個(gè)方法了。調(diào)用RequestBuilder的into方法:

image.png

into方法首先會(huì)檢測(cè)是否在主線程专控,如果不是在主線程中調(diào)用的into方法崩瓤,則直接拋出異常:IllegalArgumentException
image.png

接著構(gòu)建requestOptions。
這兩步完成后踩官,調(diào)用into的重載方法傳入創(chuàng)建好的ViewTarget以及主線程池(這是第四個(gè)線程池了却桶,前面創(chuàng)建Glide的時(shí)候,創(chuàng)建了三個(gè)線程池)蔗牡。
這個(gè)主線程池會(huì)將所有的任務(wù)都傳到主線程中進(jìn)行處理颖系。
image.png

在其重載的into方法中:
image.png

image.png

會(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
image.png

我們依次分析。

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方法

image.png

當(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里:
image.png

image.png

初始化完成回調(diào):
image.png

image.png

image.png

所以要看圖片怎么加載的豪嚎,還是要看onSizeReady方法里怎么處理的:
View初始化完成后,調(diào)用Engine#load方法加載圖片:
image.png

Engine#load方法中會(huì)根據(jù)寬高谈火、簽名侈询、request options等參數(shù)構(gòu)建EngineKey,并以此為鍵去緩存中查找是否有文件緩存糯耍,調(diào)用的是#loadFromMemory方法扔字。如果沒有緩存,則使用現(xiàn)存或者開啟新的任務(wù)來加載圖片温技。
image.png

我們接下里分析一下Engine#loadFromMemory革为,也就是Glide的緩存處理:
image.png

可以看到這里有兩級(jí)緩存,先是從#loadFromActiveResources中獲取EngineResource(ActiveResource中有個(gè)HashMap舵鳞,value是我們圖片的key震檩,value是ResourceWeakRefenerce,ResourceWeakReference中持有了我們的圖片的Resource對(duì)象)。
image.png

image.png

如果支持內(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)值**

image.png
image.png

image.png
我們來仔細(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()方法中查看了:

image.png

拿到DataFetcher對(duì)象浪汪,調(diào)用runWrapped方法-->runGenerators()-->DataFetcherGenerator#startNext()--->SourceGenerator#startNextLoad():
image.png

image.png

最后調(diào)用到DataFetcher#loadData()方法
DataFetcher是一個(gè)接口,負(fù)責(zé)數(shù)據(jù)加載凛虽。其實(shí)現(xiàn)類如下:
image.png

其網(wǎng)絡(luò)加載的實(shí)現(xiàn)類锌杀,基本上就可以確定是HttpUrlFetcher.
image.png

果不其然,看一下其#loadData方法:
image.png

調(diào)用#loadDataWithRedirects方法獲取輸入流嫉沽,并調(diào)用
DataCallback#onDataReady回調(diào)昔逗,如果IO異常,調(diào)用onLoadFailed方法至非。
在loadDataWithRedirects方法中建立HttpURLConnection加載圖片:

image.png

這就是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)很多:

image.png

image.png

我們這里只看幾個(gè)常用到的:
1.CustomViewTarget & ViewTarget
抽象類涨颜,負(fù)責(zé)加載Bitmap牵触、Drawable并且放到View上。
image.png

ViewTarget:所有View相關(guān)的Target都是繼承ViewTarget咐低,但是已經(jīng)被標(biāo)記為過期類揽思,推薦將ViewTarget替換成CustomViewTarget。
image.png

image.png

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è)資源等等。

image.png

加載完成后通過DataFetcher$DataCallback接口回調(diào)噩翠。
此接口有兩個(gè)方法


image.png

分別代表數(shù)據(jù)加載成功或者加載失敗回調(diào)戏自。

2.Encoder

也是一個(gè)接口。用來將給定的數(shù)據(jù)寫入文件中

image.png

如注釋所寫伤锚,就是把data存入文件中擅笔。
數(shù)據(jù)加載完成后會(huì)先使用Encoder將數(shù)據(jù)存入本地磁盤緩存文件中。
緩存目錄见芹,我查看的,可能是下面這個(gè):
image.png

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生命周期管理】

image.png

image.png

Glide的生命周期處理邏輯由RequestManagerRetriever#get方法出發(fā):
此方法有五個(gè)重載方法:

image.png

我們主要分析傳入FragmentActivity參數(shù)的#get方法
1.如果當(dāng)前是在子線程中就是用Application的context拇勃,也就是生命周期和Application保持一致四苇。
2.如果不是在子線程中,則先判斷activity是否銷毀方咆。拿到當(dāng)前Activity的FragmentManager月腋,通過#supportFragmentGet方法創(chuàng)建一個(gè)Fragment。

圖一

image.png

可以看到調(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)。
image.png

image.png

我們看一下RequestManager是怎么通過RequestManagerFactory#build創(chuàng)建的

image.png

通過默認(rèn)的RequestManagerFactory: DEFAULT_FACTORY的build方法中直接new了一個(gè)RequestManager苫纤〉锬疲【這個(gè)寫法蠻有意思的,之前沒這么寫過卷拘,抄喊废,必須抄】


image.png

看一下RequestManager構(gòu)造方法里的邏輯處理:


image.png

將當(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)方法颓屑。

image.png

image.png

可以看到在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)流程

image.png

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)整空間大小

image.png

回顧一下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類中。

image.png

image.png

從其他渠道笨枯,比如網(wǎng)絡(luò)或者磁盤中獲取到一個(gè)輸入流 InputStream 之后就可以進(jìn)行圖片加載了薪丁。執(zhí)行流程在其#decodeFromWrappedStreams方法中:
image.png

image.png

首先通過設(shè)置inJustDecodeBounds讀取圖片的原始尺寸信息
image.png

根據(jù)要求計(jì)算需要記載的圖片大小和config,計(jì)算結(jié)果直接設(shè)置給bitmap的options
image.png

image.png

根據(jù)圖片的期望尺寸到BitmapPool獲取一個(gè)Bitmap以復(fù)用馅精。
image.png

然后執(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)用:


image.png
image.png

其中調(diào)用了Glide的trimeMomery方法胰蝠,此方法中會(huì)清理memoryCache、bitmapPool震蒋、arrayPool茸塞。


image.png
image.png

memoryCache會(huì)調(diào)用LruResourceCache的trimMemory,最終調(diào)用LruCache的trimToSize方法清除內(nèi)存緩存查剖。

image.png

image.png
image.png
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ì)說哆键。

image.png

除了這個(gè)map掘托,還有兩個(gè)參數(shù)比較重要:
size:當(dāng)前緩存已使用大小。
maxSize:LruCache能使用的內(nèi)存的最大值籍嘹。
先分析其get和put方法:
1.get()
image.png

通過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()

image.png

將創(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瑟捣。

image.png

LinkedHashMapEntry的定義
LinkedHashMap內(nèi)部是使用雙向循環(huán)鏈表來存儲(chǔ)數(shù)據(jù)的。也就是每一個(gè)元素都持有它上一個(gè)元素地址和下一個(gè)元素地址栅干,元素實(shí)體類就是LinkedHashMapEntry迈套。
其定義如下:
image.png

當(dāng)集合的get方法被調(diào)用時(shí),會(huì)調(diào)用afterNodeAccess方法(jdk中是recordAccess)碱鳞。如果accessOrder為true桑李,就把這個(gè)元素放在集合的最末端。
image.png

LinkedHashMap#get()方法內(nèi)容很簡(jiǎn)單:
image.png

就是判斷accessOrder是否為true窿给,如果為true贵白,調(diào)用afterNodeAccess,然后返回節(jié)點(diǎn)Node的value崩泡。
排序過程:
①當(dāng)LinkedHashMap初始化的時(shí)候會(huì)初始化一個(gè)頭結(jié)點(diǎn)head;
image.png

這個(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庶弃,返回最近最少使用的元素:
image.png

返回的就是鏈表的頭結(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),我就直接截圖了:


image.png

image.png

image.png

image.png

image.png

這里L(fēng)ruCache大小為運(yùn)行緩存的八分之一忽肛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末村砂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屹逛,更是在濱河造成了極大的恐慌础废,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罕模,死亡現(xiàn)場(chǎng)離奇詭異评腺,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)淑掌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門蒿讥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抛腕,你說我怎么就攤上這事诈悍。” “怎么了兽埃?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵侥钳,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我柄错,道長(zhǎng)舷夺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任售貌,我火速辦了婚禮给猾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘颂跨。我一直安慰自己敢伸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布恒削。 她就那樣靜靜地躺著池颈,像睡著了一般尾序。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上躯砰,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天每币,我揣著相機(jī)與錄音,去河邊找鬼琢歇。 笑死兰怠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的李茫。 我是一名探鬼主播揭保,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼魄宏!你這毒婦竟也來了掖举?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤娜庇,失蹤者是張志新(化名)和其女友劉穎塔次,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體名秀,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡励负,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了匕得。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片继榆。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖汁掠,靈堂內(nèi)的尸體忽然破棺而出略吨,到底是詐尸還是另有隱情,我是刑警寧澤考阱,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布翠忠,位于F島的核電站,受9級(jí)特大地震影響乞榨,放射性物質(zhì)發(fā)生泄漏秽之。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一吃既、第九天 我趴在偏房一處隱蔽的房頂上張望考榨。 院中可真熱鬧,春花似錦鹦倚、人聲如沸河质。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掀鹅。三九已至散休,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淫半,已是汗流浹背溃槐。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工匣砖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留科吭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓猴鲫,卻偏偏與公主長(zhǎng)得像对人,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拂共,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

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