Netty源码实战(十) - 性能优化
1 性能優(yōu)化工具類
1.1 FastThreadLocal
1.1.1 傳統(tǒng)的ThreadLocal
ThreadLocal最常用的兩個(gè)接口是set和get
最常見的應(yīng)用場(chǎng)景為在線程上下文之間傳遞信息,使得用戶不受復(fù)雜代碼邏輯的影響
我們使用set的時(shí)候?qū)嶋H上是獲取Thread對(duì)象的threadLocals屬性,把當(dāng)前ThreadLocal當(dāng)做參數(shù)然后調(diào)用其set(ThreadLocal,Object)方法來設(shè)值
threadLocals是ThreadLocal.ThreadLocalMap類型的
每個(gè)線程對(duì)象關(guān)聯(lián)著一個(gè)ThreadLocalMap實(shí)例,主要是維護(hù)著一個(gè)Entry數(shù)組
Entry是擴(kuò)展了WeakReference,提供了一個(gè)存儲(chǔ)value的地方
一個(gè)線程對(duì)象可以對(duì)應(yīng)多個(gè)ThreadLocal實(shí)例,一個(gè)ThreadLocal也可以對(duì)應(yīng)多個(gè)Thread對(duì)象,當(dāng)一個(gè)Thread對(duì)象和每一個(gè)ThreadLocal發(fā)生關(guān)系的時(shí)候會(huì)生成一個(gè)Entry,并將需要存儲(chǔ)的值存儲(chǔ)在Entry的value內(nèi)
- 一個(gè)ThreadLocal對(duì)于一個(gè)Thread對(duì)象來說只能存儲(chǔ)一個(gè)值,為Object型
- 多個(gè)ThreadLocal對(duì)于一個(gè)Thread對(duì)象,這些ThreadLocal和線程相關(guān)的值存儲(chǔ)在Thread對(duì)象關(guān)聯(lián)的ThreadLocalMap中
- 使用擴(kuò)展WeakReference的Entry作為數(shù)據(jù)節(jié)點(diǎn)在一定程度上防止了內(nèi)存泄露
- 多個(gè)Thread線程對(duì)象和一個(gè)ThreadLocal發(fā)生關(guān)系的時(shí)候其實(shí)真實(shí)數(shù)據(jù)的存儲(chǔ)是跟著線程對(duì)象走的,因此這種情況不討論
我們?cè)诳纯碩hreadLocalMap#set:
Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;} } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();每個(gè)ThreadLocal實(shí)例都有一個(gè)唯一的threadLocalHashCode初始值
上面首先根據(jù)threadLocalHashCode值計(jì)算出i,有下面兩種情況會(huì)進(jìn)入for循環(huán):
- 由于threadLocalHashCode &(len-1)對(duì)應(yīng)的槽有內(nèi)容,因此滿足tab[i]!=null條件,進(jìn)入for循環(huán),如果滿足條件且當(dāng)前key不是當(dāng)前threadlocal只能說明hash沖突了
- ThreadLocal實(shí)例之前被設(shè)置過,因此滿足tab[i]!=null條件,進(jìn)入for循環(huán)
進(jìn)入for循環(huán)會(huì)遍歷tab數(shù)組,如果遇到以當(dāng)前threadLocal為key的槽,即上面第(2)種情況,有則直接將值替換
如果找到了一個(gè)已經(jīng)被回收的ThreadLocal對(duì)應(yīng)的槽,也就是當(dāng)key==null的時(shí)候表示之前的threadlocal已經(jīng)被回收了,但是value值還存在,這也是ThreadLocal內(nèi)存泄露的地方。碰到這種情況,則會(huì)引發(fā)替換這個(gè)位置的動(dòng)作
如果上面兩種情況都沒發(fā)生,即上面的第(1)種情況,則新創(chuàng)建一個(gè)Entry對(duì)象放入槽中
當(dāng)命中的時(shí)候,也就是根據(jù)當(dāng)前ThreadLocal計(jì)算出來的i恰好是當(dāng)前ThreadLocal設(shè)置的值的時(shí)候,可以直接根據(jù)hashcode來計(jì)算出位置,當(dāng)沒有命中的時(shí)候,這里沒有命中分為三種情況:
- 當(dāng)前ThreadLocal之前沒有設(shè)值過,并且當(dāng)前槽位沒有值。
- 當(dāng)前槽位有值,但是對(duì)于的不是當(dāng)前threadlocal,且那個(gè)ThreadLocal沒有被回收。
- 當(dāng)前槽位有值,但是對(duì)于的不是當(dāng)前threadlocal,且那個(gè)ThreadLocal被回收了。
上面三種情況都會(huì)調(diào)用getEntryAfterMiss方法。調(diào)用getEntryAfterMiss方法會(huì)引發(fā)數(shù)組的遍歷。
總結(jié)一下ThreadLocal的性能,一個(gè)線程對(duì)應(yīng)多個(gè)ThreadLocal實(shí)例的場(chǎng)景中
在沒有命中的情況下基本上一次hash就可以找到位置
如果發(fā)生沒有命中的情況,則會(huì)引發(fā)性能會(huì)急劇下降,當(dāng)在讀寫操作頻繁的場(chǎng)景,這點(diǎn)將成為性能詬病。
1.1.2 實(shí)例
兩個(gè)線程操作同一object 對(duì)象,顯然非線程安全,但是由于使用了 FTL, 線程安全!
結(jié)果表明內(nèi)存地址不同,并非操作同一個(gè) object!
讓T1每1s 中新生成一個(gè) object 對(duì)象
T2驗(yàn)證當(dāng)前 object 是否與之前狀態(tài)相同
顯然,每個(gè)線程拿到的對(duì)象都是線程獨(dú)享的!
某線程對(duì)變量的修改不影響其他線程!
通過對(duì)象隔離優(yōu)化了程序性能!
1.1.3 Netty FastThreadLocal源碼解析
1.1.3.1 創(chuàng)建
創(chuàng)建時(shí)重寫一下初始值方法
實(shí)際上在構(gòu)造FastThreadLocal實(shí)例的時(shí)候就決定了這個(gè)實(shí)例的索引
index 為 private 且非 static, 說明每個(gè)實(shí)例都有該值
再看看索引的生成相關(guān)代碼
index 從0開始計(jì)數(shù)
nextIndex是InternalThreadLocalMap父類的一個(gè)全局靜態(tài)的AtomicInteger類型的對(duì)象,這意味著所有的FastThreadLocal實(shí)例將共同依賴這個(gè)指針來生成唯一的索引,而且是線程安全的
Netty重新設(shè)計(jì)了更快的FastThreadLocal,主要實(shí)現(xiàn)涉及
- FastThreadLocalThread
- FastThreadLocal
- InternalThreadLocalMap
FastThreadLocalThread是Thread類的簡(jiǎn)單擴(kuò)展,主要是為了擴(kuò)展threadLocalMap屬性
FastThreadLocal提供的接口和傳統(tǒng)的ThreadLocal一致,主要是set和get方法,用法也一致
不同地方在于FastThreadLocal的值是存儲(chǔ)在InternalThreadLocalMap這個(gè)結(jié)構(gòu)里面的,傳統(tǒng)的ThreadLocal性能槽點(diǎn)主要是在讀寫的時(shí)候hash計(jì)算和當(dāng)hash沒有命中的時(shí)候發(fā)生的遍歷,我們來看看FastThreadLocal的核心實(shí)現(xiàn)
InternalThreadLocalMap實(shí)例和Thread對(duì)象一一對(duì)應(yīng)
UnpaddedInternalThreadLocalMap維護(hù)著一個(gè)數(shù)組:
這個(gè)數(shù)組用來存儲(chǔ)跟同一個(gè)線程關(guān)聯(lián)的多個(gè)FastThreadLocal的值,由于FastThreadLocal對(duì)應(yīng)indexedVariables的索引是確定的,因此在讀寫的時(shí)候?qū)?huì)發(fā)生隨機(jī)存取,非??臁?/p>
另外這里有一個(gè)問題,nextIndex是靜態(tài)唯一的,而indexedVariables數(shù)組是實(shí)例對(duì)象的,因此我認(rèn)為隨著FastThreadLocal數(shù)量的遞增,這會(huì)造成空間的浪費(fèi)
1.1.3.2 get方法實(shí)現(xiàn)
獲取 ThreadLocalMap
首先拿到當(dāng)前線程,再判斷是否為 FTL 線程快速獲取否則慢速獲取
- 讓我們先分析一下 slowGet方法
首先會(huì)獲取一個(gè) ThreadLocal 變量
拿到 JDK 的 ThreadLocal 變量,用于給每個(gè)線程拿到InternalThreadLocalMap變量,所以過程較慢,該方法稱為 slowGet 可想而知!
由于在創(chuàng)建 ThreadLocal 時(shí),并沒有重寫 initValue 方法,所以可能為 null - 接下啦看 fastGet 方法
直接通過索引取出對(duì)象
通過每個(gè)線程獨(dú)享的 ThreadLocalMap 對(duì)象借助在 JVM 中每個(gè) FTL 的唯一索引
1.2 輕量級(jí)對(duì)象池 Recycler
1.2.1 Recycler的使用
所以不使用 new 而是直接復(fù)用
Netty使用
1.2.2 Recycler的創(chuàng)建
- 創(chuàng)建方式為直接new 一個(gè) Recycler 對(duì)象,然后重寫 newObject 方法
- 轉(zhuǎn)到構(gòu)造方法
再看看每個(gè)Recycler 的結(jié)構(gòu)是如何的
- 每個(gè)Recycler 中對(duì)應(yīng)每條線程都持有一個(gè) Stack 對(duì)象
- 下面圖示說明
看下 Stack對(duì)象 的 element 參數(shù),一些默認(rèn)處理器的數(shù)組,該數(shù)組實(shí)際存放對(duì)象池,每個(gè)處理器都包裝了一個(gè)對(duì)象,handler 可被外部對(duì)象引用.,從而回收該對(duì)象
-
參數(shù)列表
-
其中,radiomask 控制對(duì)象回收的比率,所以并非每次調(diào)用recycler 都會(huì)發(fā)生回收
-
maxcapacity 池子最大元素容量
-
線程1殘留的對(duì)象會(huì)緩存到線程2中繼續(xù)釋放
所以 maxdelayedqueues 也就是可以緩存對(duì)象的線程數(shù).如若再有個(gè)線程3,而隊(duì)列結(jié)構(gòu)在線程2,那3會(huì)直接拋棄1的殘留對(duì)象. -
availablesharedcapacity:線程1中創(chuàng)建的對(duì)象能夠在其他線程中緩存的對(duì)象的最大個(gè)數(shù).
以上即為 Stack 對(duì)象所有成員變量.
下面回到Recycler的構(gòu)造方法,看其入?yún)?
該數(shù)值即為Stack 數(shù)組元素能有多少個(gè)
- 再看看如下構(gòu)造方法的參數(shù).
- 默認(rèn)值為2
- 看看新的構(gòu)造器的 radio 參數(shù)
- 默認(rèn)值8
- 兩倍CPU核心數(shù)
- 自然該值為7
1.2.3 回收對(duì)象到 Recycler
1.2.3.1 同線程回收
- 客戶端開始調(diào)用
- Recycler抽象類
- 將當(dāng)前對(duì)象壓棧
- 如下,首先判斷當(dāng)前線程,thread 即為S tack 對(duì)象中保存的成員變量,若是創(chuàng)建該 stack 的線程,則直接壓棧Stack 中,若不是再 pushlater.先分析 pushnow.
-
首先驗(yàn)證兩個(gè) id,由于默認(rèn)初始值為0,所以通過判斷.
-
接下來將其都賦值為第三個(gè) id,該值在整個(gè)Recycler 中都是唯一確定值
緊接著判斷當(dāng)前 size是否已到 maxcapacity,若達(dá)到上限,直接丟棄該對(duì)象即直接 return;否則繼續(xù)判斷 drop 處理器
首先判斷,若該對(duì)象之前未被回收過,繼續(xù)判斷;
至今已經(jīng)回收了多少個(gè)對(duì)象,其中 rm 為7,即111(二進(jìn)制表示),即每隔8個(gè)對(duì)象,就會(huì)來此判斷一次,將其與7進(jìn)行與運(yùn)算后,若不為0,則返回 true,表示只回收八分之一的對(duì)象.
繼續(xù)回到 pushnow 的流程,接下來判斷 size 是否等于數(shù)組的容量.
因?yàn)?element 是一個(gè)數(shù)組,并不是一開始就創(chuàng)建maxcapacity 容量大小,若容量不夠了,則進(jìn)行兩倍大小擴(kuò)容,再將其加入數(shù)組.
1.2.3.2 異線程回收對(duì)象
- 本節(jié)食用指南
1.2.3.2.1 獲取 WeakOrderQueue(以下簡(jiǎn)稱WOQ)
由前面 pushnow 進(jìn)入同線程回收, pushlater 即進(jìn)入異線程回收過程.
- 先看看這么個(gè)東西是啥
其類型就很神奇了,首先是個(gè)FTL,即每個(gè)線程都有一個(gè) map,map 的key=stack 表示對(duì)于不同線程,對(duì)不同 stack 來說對(duì)應(yīng)于不同的WOQ.
那么它為何要定義成一個(gè) map 結(jié)構(gòu)呢,假設(shè)有3個(gè)線程T1/2/3;
T1創(chuàng)建的對(duì)象肯可能跑到T3中回收,T2中創(chuàng)建的對(duì)象也可能到了T3回收.
那么元素就是T1以及T2的WOQ.
假設(shè)當(dāng)前在T2中,接下呢就通過get(this)拿到T1的WOQ,其中的 this 指的就是T1的 Stack.
然后若 queue==null,即表示T2從未回收過T1的對(duì)象,接下來開始判斷
當(dāng)前的即T2已經(jīng)回收過的線程數(shù) size,若不小于 mDQ,說明已經(jīng)不能再回收其他線程的對(duì)象了!
給WOQ設(shè)置 dummy 標(biāo)志,即對(duì)應(yīng)下面的若下次看到一個(gè)線程標(biāo)志了 dummy 直接return;什么也不做.
以上即為第一個(gè)過程,從FTL中拿一個(gè)Stack 對(duì)應(yīng)的WOQ.
1.2.3.2.2 創(chuàng)建 WeakOrderQueue
若之前沒拿到呢,那就直接創(chuàng)建一個(gè)WOQ吧!
- 接下來讓我們看看一個(gè)線程創(chuàng)建WOQ是如何與待回收對(duì)象的Stack 進(jìn)行綁定的.
其中的 this 即為 stack,是在T1中維護(hù)的,thread 即表示當(dāng)前線程T2.
allocat就是為了給當(dāng)前線程T2分配一個(gè)在T1中的Stack 對(duì)應(yīng)的一個(gè)WOQ.
首先判斷,T1中的 Stack還能否再分配LINK_capacity 個(gè)內(nèi)存,若不能直接返回 null;
若可以,就直接 new 一個(gè)WOQ.
讓我們具體看看其實(shí)現(xiàn).
此函數(shù)意義為:該 Stack 允許外部線程給它緩存多少個(gè)對(duì)象
經(jīng)過CAS操作設(shè)置該值為Stack 可為其他線程共享的回收對(duì)象的個(gè)數(shù).
容量足夠,則直接創(chuàng)建一個(gè)WOQ,下面來看看其數(shù)據(jù)結(jié)構(gòu).
一個(gè)鏈表結(jié)構(gòu).將其 handle與Link 進(jìn)行分離,極大地提升了性能,
因?yàn)椴槐嘏袛喈?dāng)前T2能否回收T1的對(duì)象,而只需判斷當(dāng)前的L ink 中是否有空的,則可直接將 hande 塞進(jìn)去.因?yàn)樵谇懊嬉淮涡缘呐袛噙^,從T1中是否能批量分配這么多對(duì)象(以減少很多操作的頻率).
使用同步,將WOQ插到Stack 的頭部.
1.2.3.2.3 將對(duì)象追加到 WeakOrderQueue
-
一開始呢,就是這么創(chuàng)建一個(gè)WOQ,默認(rèn)有16個(gè) handle
-
T2已經(jīng)拿到queue,接著就是添加元素.
首先設(shè)置 上次回收 id.
- 該 id 為WOQ的 id,所以是以WOQ為基礎(chǔ)的
然后拿到尾指針,獲取Link 的長(zhǎng)度,若已經(jīng)等于 link_capacity,說明已經(jīng)不可寫了;
繼續(xù)判斷 想辦法看看T1是否還能再分配一個(gè)Link來保存待回收的對(duì)象.
不允許,則直接丟棄;
允許,則直接創(chuàng)建Link并重新賦值 tail 節(jié)點(diǎn).
創(chuàng)建完后,拿到其寫指針,即 tail 的長(zhǎng)度(0).所以 tail 節(jié)點(diǎn)也已經(jīng)又有了足夠的存儲(chǔ)空間,將 handle 追加進(jìn)去.再將該 handle 的 stack 指針重置為 null,因?yàn)橐呀?jīng)不屬于原來的 stack 了.
最后,寫指針+1.
1.2.4 從 Recycler 獲取對(duì)象
本節(jié)分析若當(dāng)前 stack 為空
若當(dāng)前線程T1去獲取對(duì)象,若 stack 中有對(duì)象,則直接拿出.T1所擁有的對(duì)象即為T1擁有的 stack 中的對(duì)象,若發(fā)現(xiàn)其中為空,會(huì)嘗試與 和T1的 stack 關(guān)聯(lián)的WOQ中的 T1創(chuàng)建的,但是在其他線程中去回收的對(duì)象.那么,T1中對(duì)象不足,就需要在其他線程中去回收.
其中的 cusor 指針即當(dāng)前所需要回收的對(duì)象
- 彈棧獲取元素
- 若 size 為0,則從其他線程回收
若已經(jīng)回收到則直接 return true.沒有則重置兩個(gè)指針,將 cusor 指向頭結(jié)點(diǎn),意味著準(zhǔn)備從頭開始回收.
接下來具體分析這段長(zhǎng)代碼
boolean scavengeSome() {WeakOrderQueue prev;// 先拿到 cusorWeakOrderQueue cursor = this.cursor;// cusor 節(jié)點(diǎn)無對(duì)象if (cursor == null) {prev = null;// 指向頭結(jié)點(diǎn)cursor = head;// 頭結(jié)點(diǎn)依舊為空,已經(jīng)沒有與之關(guān)聯(lián)的WOQ,直接返回 false.if (cursor == null) {return false;}} else {prev = this.prev;}boolean success = false;// 此處 do/while 循環(huán)只為去尋找與 stack 關(guān)聯(lián)的WOQ,看看到底能不能碰到一個(gè)對(duì)象.do {// transfer 即為了將WOQ中的對(duì)象傳輸?shù)?stack 中.成功獲取則結(jié)束循環(huán)!if (cursor.transfer(this)) {success = true;break;}// 沒有回收成功,則看往 cusor 的下一個(gè)節(jié)點(diǎn)WeakOrderQueue next = cursor.next;// owner 為與當(dāng)前WOQ關(guān)聯(lián)的一個(gè)線程(對(duì)應(yīng)圖中的T4)// 為空,說明T4已經(jīng)不存在!隨后即,做一些善后清理工作if (cursor.owner.get() == null) {// If the thread associated with the queue is gone, unlink it, after// performing a volatile read to confirm there is no data left to collect.// We never unlink the first queue, as we don't want to synchronize on updating the head.// 判斷節(jié)點(diǎn)中是否還有數(shù)據(jù)if (cursor.hasFinalData()) {// 就需要想辦法將數(shù)據(jù)傳輸?shù)?stack 中for (;;) {if (cursor.transfer(this)) {success = true;} else {break;}}}// 處理完該節(jié)點(diǎn)后,即將其刪除,通過傳統(tǒng)的指針的刪除方法if (prev != null) {prev.setNext(next);}// T4還存活,繼續(xù)看后繼節(jié)點(diǎn).} else {prev = cursor;}cursor = next;// cusor 為空時(shí),誒就結(jié)束循環(huán)啦!} while (cursor != null && !success);this.prev = prev;this.cursor = cursor;return success;}- 下面看傳輸方法
1.3 小結(jié)
參考
Java讀源碼之Netty深入剖析
總結(jié)
以上是生活随笔為你收集整理的Netty源码实战(十) - 性能优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 华为计算机视觉算法,【华为图像算法面试】
- 下一篇: 如何导入BurpSuite 证书