本文基于 Netty 4.1.56.Final 版本進(jìn)行討論
在上篇文章《聊一聊 Netty 數(shù)據(jù)搬運(yùn)工 ByteBuf 體系的設(shè)計(jì)與實(shí)現(xiàn)》 中,筆者詳細(xì)地為大家介紹了 ByteBuf 整個(gè)體系的設(shè)計(jì),其中筆者覺得 Netty 對(duì)于引用計(jì)數(shù)的設(shè)計(jì)非常精彩,因此將這部分設(shè)計(jì)內(nèi)容專門獨(dú)立出來(lái)。
Netty 為 ByteBuf 引入了引用計(jì)數(shù)的機(jī)制,在 ByteBuf 的整個(gè)設(shè)計(jì)體系中,所有的 ByteBuf 都會(huì)繼承一個(gè)抽象類 AbstractReferenceCountedByteBuf , 它是對(duì)接口 ReferenceCounted 的實(shí)現(xiàn)。

public interface ReferenceCounted {
int refCnt();
ReferenceCounted retain();
ReferenceCounted retain(int increment);
boolean release();
boolean release(int decrement);
}
每個(gè) ByteBuf 的內(nèi)部都維護(hù)了一個(gè)叫做 refCnt 的引用計(jì)數(shù),我們可以通過(guò) refCnt() 方法來(lái)獲取 ByteBuf 當(dāng)前的引用計(jì)數(shù) refCnt。當(dāng) ByteBuf 在其他上下文中被引用的時(shí)候,我們需要通過(guò) retain() 方法將 ByteBuf 的引用計(jì)數(shù)加 1。另外我們也可以通過(guò) retain(int increment) 方法來(lái)指定 refCnt 增加的大小(increment)。
有對(duì) ByteBuf 的引用那么就有對(duì) ByteBuf 的釋放,每當(dāng)我們使用完 ByteBuf 的時(shí)候就需要手動(dòng)調(diào)用 release() 方法將 ByteBuf 的引用計(jì)數(shù)減 1 。當(dāng)引用計(jì)數(shù) refCnt 變成 0 的時(shí)候,Netty 就會(huì)通過(guò) deallocate 方法來(lái)釋放 ByteBuf 所引用的內(nèi)存資源。這時(shí) release() 方法會(huì)返回 true , 如果 refCnt 還不為 0 ,那么就返回 false 。同樣我們也可以通過(guò) release(int decrement) 方法來(lái)指定 refCnt 減少多少(decrement)。
1. 為什么要引入引用計(jì)數(shù)
”在其他上下文中引用 ByteBuf “ 是什么意思呢 ? 比如我們?cè)诰€程 1 中創(chuàng)建了一個(gè) ByteBuf,然后將這個(gè) ByteBuf 丟給線程 2 進(jìn)行處理,線程 2 又可能丟給線程 3, 而每個(gè)線程都有自己的上下文處理邏輯,比如對(duì) ByteBuf 的處理,釋放等操作。這樣就使得 ByteBuf 在事實(shí)上形成了在多個(gè)線程上下文中被共享的情況。
面對(duì)這種情況我們就很難在一個(gè)單獨(dú)的線程上下文中判斷一個(gè) ByteBuf 該不該被釋放,比如線程 1 準(zhǔn)備釋放 ByteBuf 了,但是它可能正在被其他線程使用。所以這也是 Netty 為 ByteBuf 引入引用計(jì)數(shù)的重要原因,每當(dāng)引用一次 ByteBuf 的時(shí)候就需要通過(guò) retain() 方法將引用計(jì)數(shù)加 1, release() 釋放的時(shí)候?qū)⒁糜?jì)數(shù)減 1 ,當(dāng)引用計(jì)數(shù)為 0 了,說(shuō)明已經(jīng)沒有其他上下文引用 ByteBuf 了,這時(shí) Netty 就可以釋放它了。
另外相比于 JDK DirectByteBuffer 需要依賴 GC 機(jī)制來(lái)釋放其背后引用的 Native Memory , Netty 更傾向于手動(dòng)及時(shí)釋放 DirectByteBuf 。因?yàn)?JDK DirectByteBuffer 的釋放需要等到 GC 發(fā)生,由于 DirectByteBuffer 的對(duì)象實(shí)例所占的 JVM 堆內(nèi)存太小了,所以一時(shí)很難觸發(fā) GC , 這就導(dǎo)致被引用的 Native Memory 的釋放有了一定的延遲,嚴(yán)重的情況會(huì)越積越多,導(dǎo)致 OOM 。而且也會(huì)導(dǎo)致進(jìn)程中對(duì) DirectByteBuffer 的申請(qǐng)操作有非常大的延遲。
而 Netty 為了避免這些情況的出現(xiàn),選擇在每次使用完畢之后手動(dòng)釋放 Native Memory ,但是不依賴 JVM 的話,總會(huì)有內(nèi)存泄露的情況,比如在使用完了 ByteBuf 卻忘記調(diào)用 release() 方法來(lái)釋放。
所以為了檢測(cè)內(nèi)存泄露的發(fā)生,這也是 Netty 為 ByteBuf 引入了引用計(jì)數(shù)的另一個(gè)原因,當(dāng) ByteBuf 不再被引用的時(shí)候,也就是沒有任何強(qiáng)引用或者軟引用的時(shí)候,如果此時(shí)發(fā)生 GC , 那么這個(gè) ByteBuf 實(shí)例(位于 JVM 堆中)就需要被回收了,這時(shí) Netty 就會(huì)檢查這個(gè) ByteBuf 的引用計(jì)數(shù)是否為 0 , 如果不為 0 ,說(shuō)明我們忘記調(diào)用 release() 釋放了,近而判斷出這個(gè) ByteBuf 發(fā)生了內(nèi)存泄露。
在探測(cè)到內(nèi)存泄露發(fā)生之后,后續(xù) Netty 就會(huì)通過(guò) reportLeak() 將內(nèi)存泄露的相關(guān)信息以 error 的日志級(jí)別輸出到日志中。
看到這里,大家可能不禁要問(wèn),不就是引入了一個(gè)小小的引用計(jì)數(shù)嘛,這有何難 ? 值得這里大書特書嗎 ? 不就是在創(chuàng)建 ByteBuf 的時(shí)候?qū)⒁糜?jì)數(shù) refCnt 初始化為 1 , 每次在其他上下文引用的時(shí)候?qū)?refCnt 加 1, 每次釋放的時(shí)候再將 refCnt 減 1 嗎 ?減到 0 的時(shí)候就釋放 Native Memory ,太簡(jiǎn)單了吧~~
事實(shí)上 Netty 對(duì)引用計(jì)數(shù)的設(shè)計(jì)非常講究,絕非如此簡(jiǎn)單,甚至有些復(fù)雜,其背后隱藏著大大的性能考究以及對(duì)復(fù)雜并發(fā)問(wèn)題的全面考慮,在性能與線程安全問(wèn)題之間的反復(fù)權(quán)衡。
2. 引用計(jì)數(shù)的最初設(shè)計(jì)
所以為了理清關(guān)于引用計(jì)數(shù)的整個(gè)設(shè)計(jì)脈絡(luò),我們需要將版本回退到最初的起點(diǎn) —— 4.1.16.Final 版本,來(lái)看一下原始的設(shè)計(jì)。
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
// 原子更新 refCnt 的 Updater
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
// 引用計(jì)數(shù),初始化為 1
private volatile int refCnt;
protected AbstractReferenceCountedByteBuf(int maxCapacity) {
super(maxCapacity);
// 引用計(jì)數(shù)初始化為 1
refCntUpdater.set(this, 1);
}
// 引用計(jì)數(shù)增加 increment
private ByteBuf retain0(int increment) {
for (;;) {
int refCnt = this.refCnt;
// 每次 retain 的時(shí)候?qū)σ糜?jì)數(shù)加 1
final int nextCnt = refCnt + increment;
// Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow.
if (nextCnt <= increment) {
// 如果 refCnt 已經(jīng)為 0 或者發(fā)生溢出,則拋異常
throw new IllegalReferenceCountException(refCnt, increment);
}
// CAS 更新 refCnt
if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
break;
}
}
return this;
}
// 引用計(jì)數(shù)減少 decrement
private boolean release0(int decrement) {
for (;;) {
int refCnt = this.refCnt;
if (refCnt < decrement) {
// 引用的次數(shù)必須和釋放的次數(shù)相等對(duì)應(yīng)
throw new IllegalReferenceCountException(refCnt, -decrement);
}
// 每次 release 引用計(jì)數(shù)減 1
// CAS 更新 refCnt
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
if (refCnt == decrement) {
// 如果引用計(jì)數(shù)為 0 ,則釋放 Native Memory,并返回 true
deallocate();
return true;
}
// 引用計(jì)數(shù)不為 0 ,返回 false
return false;
}
}
}
}
在 4.1.16.Final 之前的版本設(shè)計(jì)中,確實(shí)和我們當(dāng)初想象的一樣,非常簡(jiǎn)單,創(chuàng)建 ByteBuf 的時(shí)候?qū)?refCnt 初始化為 1。 每次引用 retain 的時(shí)候?qū)⒁糜?jì)數(shù)加 1 ,每次釋放 release 的時(shí)候?qū)⒁糜?jì)數(shù)減 1,在一個(gè) for 循環(huán)中通過(guò) CAS 替換。當(dāng)引用計(jì)數(shù)為 0 的時(shí)候,通過(guò) deallocate() 釋放 Native Memory。
3. 引入指令級(jí)別上的優(yōu)化
4.1.16.Final 的設(shè)計(jì)簡(jiǎn)潔清晰,在我們看來(lái)完全沒有任何問(wèn)題,但 Netty 對(duì)性能的考究完全沒有因此止步,由于在 x86 架構(gòu)下 XADD 指令的性能要高于 CMPXCHG 指令, compareAndSet 方法底層是通過(guò) CMPXCHG 指令實(shí)現(xiàn)的,而 getAndAdd 方法底層是 XADD 指令。
所以在對(duì)性能極致的追求下,Netty 在 4.1.17.Final 版本中用 getAndAdd 方法來(lái)替換 compareAndSet 方法。
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
private volatile int refCnt;
protected AbstractReferenceCountedByteBuf(int maxCapacity) {
super(maxCapacity);
// 引用計(jì)數(shù)在初始的時(shí)候還是為 1
refCntUpdater.set(this, 1);
}
private ByteBuf retain0(final int increment) {
// 相比于 compareAndSet 的實(shí)現(xiàn),這里將 for 循環(huán)去掉
// 并且每次是先對(duì) refCnt 增加計(jì)數(shù) increment
int oldRef = refCntUpdater.getAndAdd(this, increment);
// 增加完 refCnt 計(jì)數(shù)之后才去判斷異常情況
if (oldRef <= 0 || oldRef + increment < oldRef) {
// Ensure we don't resurrect (which means the refCnt was 0) and also that we encountered an overflow.
// 如果原來(lái)的 refCnt 已經(jīng)為 0 或者 refCnt 溢出,則對(duì) refCnt 進(jìn)行回退,并拋出異常
refCntUpdater.getAndAdd(this, -increment);
throw new IllegalReferenceCountException(oldRef, increment);
}
return this;
}
private boolean release0(int decrement) {
// 先對(duì) refCnt 減少計(jì)數(shù) decrement
int oldRef = refCntUpdater.getAndAdd(this, -decrement);
// 如果 refCnt 已經(jīng)為 0 則進(jìn)行 Native Memory 的釋放
if (oldRef == decrement) {
deallocate();
return true;
} else if (oldRef < decrement || oldRef - decrement > oldRef) {
// 如果釋放次數(shù)大于 retain 次數(shù) 或者 refCnt 出現(xiàn)下溢
// 則對(duì) refCnt 進(jìn)行回退,并拋出異常
refCntUpdater.getAndAdd(this, decrement);
throw new IllegalReferenceCountException(oldRef, decrement);
}
return false;
}
}
在 4.1.16.Final 版本的實(shí)現(xiàn)中,Netty 是在一個(gè) for 循環(huán)中,先對(duì) retain 和 release 的異常情況進(jìn)行校驗(yàn),之后再通過(guò) CAS 更新 refCnt。否則直接拋出 IllegalReferenceCountException。采用的是一種悲觀更新引用計(jì)數(shù)的策略。
而在 4.1.17.Final 版本的實(shí)現(xiàn)中 , Netty 去掉了 for 循環(huán),正好和 compareAndSet 的實(shí)現(xiàn)相反,而是先通過(guò) getAndAdd 更新 refCnt,更新之后再來(lái)判斷相關(guān)的異常情況,如果發(fā)現(xiàn)有異常,則進(jìn)行回退,并拋出 IllegalReferenceCountException。采用的是一種樂(lè)觀更新引用計(jì)數(shù)的策略。
比如在 retain 增加引用計(jì)數(shù)的時(shí)候,先對(duì) refCnt 增加計(jì)數(shù) increment,然后判斷原來(lái)的引用計(jì)數(shù) oldRef 是否已經(jīng)為 0 或者 refCnt 是否發(fā)生溢出,如果是,則需要對(duì) refCnt 的值進(jìn)行回退,并拋異常。
在 release 減少引用計(jì)數(shù)的時(shí)候,先對(duì) refCnt 減少計(jì)數(shù) decrement,然后判斷 release 的次數(shù)是否大于 retain 的次數(shù)防止 over-release ,以及 refCnt 是否發(fā)生下溢,如果是,則對(duì) refCnt 的值進(jìn)行回退,并拋異常。
4. 并發(fā)安全問(wèn)題的引入
在 4.1.17.Final 版本的設(shè)計(jì)中,我們對(duì)引用計(jì)數(shù)的 retain 以及 release 操作都要比 4.1.16.Final 版本的性能要高,雖然現(xiàn)在性能是高了,但是同時(shí)引入了新的并發(fā)問(wèn)題。
讓我們先假設(shè)一個(gè)這樣的場(chǎng)景,現(xiàn)在有一個(gè) ByteBuf,它當(dāng)前的 refCnt = 1 ,線程 1 對(duì)這個(gè) ByteBuf 執(zhí)行 release() 操作。

在 4.1.17.Final 的實(shí)現(xiàn)中,Netty 會(huì)首先通過(guò) getAndAdd 將 refCnt 更新為 0 ,然后接著調(diào)用 deallocate() 方法釋放 Native Memory ,很簡(jiǎn)單也很清晰是吧,讓我們?cè)偌狱c(diǎn)并發(fā)復(fù)雜度上去。
現(xiàn)在我們?cè)谏蠄D步驟一與步驟二之間插入一個(gè)線程 2 , 線程 2 對(duì)這個(gè) ByteBuf 并發(fā)執(zhí)行 retain() 方法。

在 4.1.17.Final 的實(shí)現(xiàn)中,線程 2 首先通過(guò) getAndAdd 將 refCnt 從 0 更新為 1,緊接著線程 2 就會(huì)發(fā)現(xiàn) refCnt 原來(lái)的值 oldRef 是等于 0 的,也就是說(shuō)線程 2 在調(diào)用 retain() 的時(shí)候,ByteBuf 的引用計(jì)數(shù)已經(jīng)為 0 了,并且線程 1 已經(jīng)開始準(zhǔn)備釋放 Native Memory 了。
所以線程 2 需要再次調(diào)用 getAndAdd 方法將 refCnt 的值進(jìn)行回退,從 1 再次回退到 0 ,最后拋出 IllegalReferenceCountException。這樣的結(jié)果顯然是正確的,也是符合語(yǔ)義的。畢竟不能對(duì)一個(gè)引用計(jì)數(shù)為 0 的 ByteBuf 調(diào)用 retain() 。
現(xiàn)在看來(lái)一切風(fēng)平浪靜,都是按照我們的設(shè)想有條不紊的進(jìn)行,我們不妨再加點(diǎn)并發(fā)復(fù)雜度上去。在上圖步驟 1.1 與步驟 1.2 之間在插入一個(gè)線程 3 , 線程 3 對(duì)這個(gè) ByteBuf 再次并發(fā)執(zhí)行 retain() 方法。

由于引用計(jì)數(shù)的更新(步驟 1.1)與引用計(jì)數(shù)的回退(步驟 1.2)這兩個(gè)操作并不是一個(gè)原子操作,如果在這兩個(gè)操作之間不巧插入了一個(gè)線程 3 ,線程 3 在并發(fā)執(zhí)行 retain() 方法的時(shí)候,首先會(huì)通過(guò) getAndAdd 將引用計(jì)數(shù) refCnt 從 1 增加到 2 。
注意,此時(shí)線程 2 還沒來(lái)得及回退 refCnt , 所以線程 3 此時(shí)看到的 refCnt 是 1 而不是 0 。
由于此時(shí)線程 3 看到的 oldRef 是 1 ,所以線程 3 成功調(diào)用 retain() 方法將 ByteBuf 的引用計(jì)數(shù)增加到了 2 ,并且不會(huì)回退也不會(huì)拋出異常。在線程 3 看來(lái)此時(shí)的 ByteBuf 完完全全是一個(gè)正??梢员皇褂玫?ByteBuf。
緊接著線程 1 開始執(zhí)行步驟 2 —— deallocate() 方法釋放 Native Memory,此后線程 3 在訪問(wèn)這個(gè) ByteBuf 的時(shí)候就有問(wèn)題了,因?yàn)? Native Memory 已經(jīng)被線程1 釋放了。
5. 在性能與并發(fā)安全之間的權(quán)衡
接下來(lái) Netty 就需要在性能與并發(fā)安全之間進(jìn)行權(quán)衡了,現(xiàn)在有兩個(gè)選擇,第一個(gè)選擇是直接回滾到 4.1.16.Final 版本,放棄 XADD 指令帶來(lái)的性能提升,之前的設(shè)計(jì)中采用的 CMPXCHG 指令雖然性能相對(duì)差一些,但是不會(huì)出現(xiàn)上述的并發(fā)安全問(wèn)題。
因?yàn)?Netty 是在一個(gè) for 循環(huán)中采用悲觀的策略來(lái)更新引用計(jì)數(shù),先是判斷異常情況,然后在通過(guò) CAS 來(lái)更新 refCnt。即使多個(gè)線程看到了 refCnt 的中間狀態(tài)也沒關(guān)系,因?yàn)榻酉聛?lái)進(jìn)行的 CAS 也會(huì)跟著失敗。
比如上邊例子中的線程 1 對(duì) ByteBuf 進(jìn)行 release 的時(shí)候,在線程 1 執(zhí)行 CAS 將 refCnt 替換為 0 之前的這個(gè)間隙中,refCnt 是 1 ,如果在這個(gè)間隙中,線程 2 并發(fā)執(zhí)行 retain 方法,此時(shí)線程 2 看到的 refCnt 確實(shí)為 1 ,它是一個(gè)中間狀態(tài),線程 2 執(zhí)行 CAS 將 refCnt 替換為 2。
此時(shí)線程 1 執(zhí)行 CAS 就會(huì)失敗,但會(huì)在下一輪 for 循環(huán)中將 refCnt 替換為 1,這是完全符合引用計(jì)數(shù)語(yǔ)義的。
另外一種情況是線程 1 已經(jīng)執(zhí)行完 CAS 將 refCnt 替換為 0 ,這時(shí)候線程 2 去 retain ,由于 4.1.16.Final 版本中的設(shè)計(jì)是先檢查異常后 CAS 替換,所以線程 2 首先會(huì)在 retain 方法中檢查到 ByteBuf 的 refCnt 已經(jīng)為 0 ,直接拋出 IllegalReferenceCountException,并不會(huì)執(zhí)行 CAS 。這同樣符合引用計(jì)數(shù)的語(yǔ)義,畢竟不能對(duì)一個(gè)引用計(jì)數(shù)已經(jīng)為 0 的 ByteBuf 執(zhí)行任何訪問(wèn)操作。
第二個(gè)選擇是既要保留 XADD 指令帶來(lái)的性能提升,也要解決 4.1.17.Final 版本中引入的并發(fā)安全問(wèn)題。毫無(wú)疑問(wèn),Netty 最終選擇的是這種方案。
在介紹 Netty 的精彩設(shè)計(jì)之前,我想我們還是應(yīng)該在回顧下這個(gè)并發(fā)安全問(wèn)題出現(xiàn)的根本原因是什么 ?
在 4.1.17.Final 版本的設(shè)計(jì)中,Netty 首先是通過(guò) getAndAdd 方法先對(duì) refCnt 的值進(jìn)行更新,如果出現(xiàn)異常情況,在進(jìn)行回滾。而更新,回滾的這兩個(gè)操作并不是原子的,之間的中間狀態(tài)會(huì)被其他線程看到。
比如,線程 2 看到了線程 1 的中間狀態(tài)(refCnt = 0),于是將引用計(jì)數(shù)加到 1
, 在線程 2 進(jìn)行回滾之前,這期間的中間狀態(tài)(refCnt = 1,oldRef = 0)又被線程 3 看到了,于是線程 3 將引用計(jì)數(shù)增加到了 2 (refCnt = 2,oldRef = 1)。 此時(shí)線程 3 覺得這是一種正常的狀態(tài),但在線程 1 看來(lái) refCnt 的值已經(jīng)是 0 了,后續(xù)線程 1 就會(huì)釋放 Native Memory ,這就出問(wèn)題了。
問(wèn)題的根本原因其實(shí)是這里的 refCnt 不同的值均代表不同的語(yǔ)義,比如對(duì)于線程 1 來(lái)說(shuō),通過(guò) release 將 refCnt 減到了 0 ,這里的語(yǔ)義是 ByteBuf 已經(jīng)不在被引用了,可以釋放 Native Memory 。
隨后線程 2 通過(guò) retain 將 refCnt 加到了 1 ,這就把 ByteBuf 語(yǔ)義改變了,表示該 ByteBuf 在線程 2 中被引用了一次。最后線程 3 又通過(guò) retain 將 refCnt 加到了 2 ,再一次改變了 ByteBuf 的語(yǔ)義。
只要用到 XADD 指令來(lái)實(shí)現(xiàn)引用計(jì)數(shù)的更新,那么就不可避免的出現(xiàn)上述并發(fā)更新 refCnt 的情況,關(guān)鍵是 refCnt 的值每一次被其他線程并發(fā)修改之后,ByteBuf 的語(yǔ)義就變了。這才是 4.1.17.Final 版本中的關(guān)鍵問(wèn)題所在。
如果 Netty 想在同時(shí)享受 XADD 指令帶來(lái)的性能提升之外,又要解決上述提到的并發(fā)安全問(wèn)題,就要重新對(duì)引用計(jì)數(shù)進(jìn)行設(shè)計(jì)。首先我們的要求是繼續(xù)采用 XADD 指令來(lái)實(shí)現(xiàn)引用計(jì)數(shù)的更新,但這就會(huì)帶來(lái)多線程并發(fā)修改所引起的 ByteBuf 語(yǔ)義改變。
既然多線程并發(fā)修改無(wú)法避免,那么我們能不能重新設(shè)計(jì)一下引用計(jì)數(shù),讓 ByteBuf 語(yǔ)義無(wú)論多線程怎么修改,它的語(yǔ)義始終保持不變。也就是說(shuō)只要線程 1 將 refCnt 減到了 0 ,那么無(wú)論線程 2 和線程 3 怎么并發(fā)修改 refCnt,怎么增加 refCnt 的值,refCnt 等于 0 的這個(gè)語(yǔ)義始終保持不變呢 ?
6. 奇偶設(shè)計(jì)的引入
這里 Netty 有一個(gè)極奇巧妙精彩的設(shè)計(jì),引用計(jì)數(shù)的設(shè)計(jì)不再是邏輯意義上的 0 , 1 , 2 , 3 .....,而是分為了兩大類,要么是偶數(shù),要么是奇數(shù)。
偶數(shù)代表的語(yǔ)義是 ByteBuf 的 refCnt 不為 0 ,也就是說(shuō)只要一個(gè) ByteBuf 還在被引用,那么它的 refCnt 就是一個(gè)偶數(shù),具體被引用多少次,可以通過(guò)
refCnt >>> 1來(lái)獲取。奇數(shù)代表的語(yǔ)義是 ByteBuf 的 refCnt 等于 0 ,只要一個(gè) ByteBuf 已經(jīng)沒有任何地方引用它了,那么它的 refCnt 就是一個(gè)奇數(shù),其背后引用的 Native Memory 隨后就會(huì)被釋放。
ByteBuf 在初始化的時(shí)候,refCnt 不在是 1 而是被初始化為 2 (偶數(shù)),每次 retain 的時(shí)候不在是對(duì) refCnt 加 1 而是加 2 (偶數(shù)步長(zhǎng)),每次 release 的時(shí)候不再是對(duì) refCnt 減 1 而是減 2 (同樣是偶數(shù)步長(zhǎng))。這樣一來(lái),只要一個(gè) ByteBuf 的引用計(jì)數(shù)為偶數(shù),那么多線程無(wú)論怎么并發(fā)調(diào)用 retain 方法,引用計(jì)數(shù)還是一個(gè)偶數(shù),語(yǔ)義仍然保持不變。
public final int initialValue() {
return 2;
}
當(dāng)一個(gè) ByteBuf 被 release 到?jīng)]有任何引用計(jì)數(shù)的時(shí)候,Netty 不在將 refCnt 設(shè)置為 0 而是設(shè)置為 1 (奇數(shù)),對(duì)于一個(gè)值為奇數(shù)的 refCnt,無(wú)論多線程怎么并發(fā)調(diào)用 retain 方法和 release 方法,引用計(jì)數(shù)還是一個(gè)奇數(shù),ByteBuf 引用計(jì)數(shù)為 0 的這層語(yǔ)義一直會(huì)保持不變。
我們還是以上圖中所展示的并發(fā)安全問(wèn)題為例,在新的引用計(jì)數(shù)設(shè)計(jì)方案中,首先線程 1 對(duì) ByteBuf 執(zhí)行 release 方法,Netty 會(huì)將 refCnt 設(shè)置為 1 (奇數(shù))。
線程 2 并發(fā)調(diào)用 retain 方法,通過(guò) getAndAdd 將 refCnt 從 1 加到了 3 ,refCnt 仍然是一個(gè)奇數(shù),按照奇數(shù)所表示的語(yǔ)義 —— ByteBuf 引用計(jì)數(shù)已經(jīng)是 0 了,那么線程 2 就會(huì)在 retain 方法中拋出 IllegalReferenceCountException。
線程 3 并發(fā)調(diào)用 retain 方法,通過(guò) getAndAdd 將 refCnt 從 3 加到了 5,看到了沒 ,在新方案的設(shè)計(jì)中,無(wú)論多線程怎么并發(fā)執(zhí)行 retain 方法,refCnt 的值一直都只會(huì)是一個(gè)奇數(shù),隨后線程 3 在 retain 方法中拋出 IllegalReferenceCountException。這完全符合引用計(jì)數(shù)的并發(fā)語(yǔ)義。
這個(gè)新的引用計(jì)數(shù)設(shè)計(jì)方案是在 4.1.32.Final 版本引入進(jìn)來(lái)的,僅僅通過(guò)一個(gè)奇偶設(shè)計(jì),就非常巧妙的解決了 4.1.17.Final 版本中存在的并發(fā)安全問(wèn)題。現(xiàn)在新方案的核心設(shè)計(jì)要素我們已經(jīng)清楚了,那么接下來(lái)筆者將以 4.1.56.Final 版本來(lái)為大家繼續(xù)介紹下新方案的實(shí)現(xiàn)細(xì)節(jié)。
Netty 中的 ByteBuf 全部繼承于 AbstractReferenceCountedByteBuf,在這個(gè)類中實(shí)現(xiàn)了所有對(duì) ByteBuf 引用計(jì)數(shù)的操作,對(duì)于 ReferenceCounted 接口的實(shí)現(xiàn)就在這里。
public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
// 獲取 refCnt 字段在 ByteBuf 對(duì)象內(nèi)存中的偏移
// 后續(xù)通過(guò) Unsafe 對(duì) refCnt 進(jìn)行操作
private static final long REFCNT_FIELD_OFFSET =
ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCountedByteBuf.class, "refCnt");
// 獲取 refCnt 字段 的 AtomicFieldUpdater
// 后續(xù)通過(guò) AtomicFieldUpdater 來(lái)操作 refCnt 字段
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> AIF_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
// 創(chuàng)建 ReferenceCountUpdater,對(duì)于引用計(jì)數(shù)的所有操作最終都會(huì)代理到這個(gè)類中
private static final ReferenceCountUpdater<AbstractReferenceCountedByteBuf> updater =
new ReferenceCountUpdater<AbstractReferenceCountedByteBuf>() {
@Override
protected AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater() {
// 通過(guò) AtomicIntegerFieldUpdater 操作 refCnt 字段
return AIF_UPDATER;
}
@Override
protected long unsafeOffset() {
// 通過(guò) Unsafe 操作 refCnt 字段
return REFCNT_FIELD_OFFSET;
}
};
// ByteBuf 中的引用計(jì)數(shù),初始為 2 (偶數(shù))
private volatile int refCnt = updater.initialValue();
}
其中定義了一個(gè) refCnt 字段用于記錄 ByteBuf 被引用的次數(shù),由于采用了奇偶設(shè)計(jì),在創(chuàng)建 ByteBuf 的時(shí)候,Netty 會(huì)將 refCnt 初始化為 2 (偶數(shù)),它的邏輯語(yǔ)義是該 ByteBuf 被引用一次。后續(xù)對(duì) ByteBuf 執(zhí)行 retain 就會(huì)對(duì) refCnt 進(jìn)行加 2 ,執(zhí)行 release 就會(huì)對(duì) refCnt 進(jìn)行減 2 ,對(duì)于引用計(jì)數(shù)的單次操作都是以 2 為步長(zhǎng)進(jìn)行。
由于在 Netty 中除了 AbstractReferenceCountedByteBuf 這個(gè)專門用于實(shí)現(xiàn) ByteBuf 的引用計(jì)數(shù)功能之外,還有一個(gè)更加通用的引用計(jì)數(shù)抽象類 AbstractReferenceCounted,它用于實(shí)現(xiàn)所有系統(tǒng)資源類的引用計(jì)數(shù)功能(ByteBuf 只是其中的一種內(nèi)存資源)。
由于都是對(duì)引用計(jì)數(shù)的實(shí)現(xiàn),所以在之前的版本中,這兩個(gè)類中包含了很多重復(fù)的引用計(jì)數(shù)相關(guān)操作邏輯,所以 Netty 在 4.1.35.Final 版本中專門引入了一個(gè) ReferenceCountUpdater 類,將所有引用計(jì)數(shù)的相關(guān)實(shí)現(xiàn)聚合在這里。
ReferenceCountUpdater 對(duì)于引用計(jì)數(shù) refCnt 的操作有兩種方式,一種是通過(guò) AtomicFieldUpdater 來(lái)對(duì) refCnt 進(jìn)行操作,我們可以通過(guò) updater() 獲取到 refCnt 字段對(duì)應(yīng)的 AtomicFieldUpdater。
另一種則是通過(guò) Unsafe 來(lái)對(duì) refCnt 進(jìn)行操作,我們可以通過(guò) unsafeOffset() 來(lái)獲取到 refCnt 字段在 ByteBuf 實(shí)例對(duì)象內(nèi)存中的偏移。
按理來(lái)說(shuō),我們采用一種方式就可以對(duì) refCnt 進(jìn)行訪問(wèn)或者更新了,那為什么 Netty 提供了兩種方式呢 ?會(huì)顯得有點(diǎn)多余嗎 ?這個(gè)點(diǎn)大家可以先思考下為什么 ,后續(xù)在我們剖析到源碼細(xì)節(jié)的時(shí)候筆者在為大家解答。
好了,下面我們正式開始介紹新版引用計(jì)數(shù)設(shè)計(jì)方案的具體實(shí)現(xiàn)細(xì)節(jié),第一個(gè)問(wèn)題,在新的設(shè)計(jì)方案中,我們?nèi)绾潍@取 ByteBuf 的邏輯引用計(jì)數(shù) ?
public abstract class ReferenceCountUpdater<T extends ReferenceCounted> {
public final int initialValue() {
// ByteBuf 引用計(jì)數(shù)初始化為 2
return 2;
}
public final int refCnt(T instance) {
// 通過(guò) updater 獲取 refCnt
// 根據(jù) refCnt 在 realRefCnt 中獲取真實(shí)的引用計(jì)數(shù)
return realRefCnt(updater().get(instance));
}
// 獲取 ByteBuf 的邏輯引用計(jì)數(shù)
private static int realRefCnt(int rawCnt) {
// 奇偶判斷
return rawCnt != 2 && rawCnt != 4 && (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1;
}
}
由于采用了奇偶引用計(jì)數(shù)的設(shè)計(jì),所以我們?cè)讷@取邏輯引用計(jì)數(shù)的時(shí)候需要判斷當(dāng)前 rawCnt(refCnt)是奇數(shù)還是偶數(shù),它們分別代表了不同的語(yǔ)義。
如果 rawCnt 是奇數(shù),則表示當(dāng)前 ByteBuf 已經(jīng)沒有任何地方引用了,邏輯引用計(jì)數(shù)返回 0.
如果 rawCnt 是偶數(shù),則表示當(dāng)前 ByteBuf 還有地方在引用,邏輯引用計(jì)數(shù)則為
rawCnt >>> 1。
realRefCnt 函數(shù)其實(shí)就是簡(jiǎn)單的一個(gè)奇偶判斷邏輯,但在它的實(shí)現(xiàn)中卻體現(xiàn)出了 Netty 對(duì)性能的極致追求。比如,我們判斷一個(gè)數(shù)是奇數(shù)還是偶數(shù)其實(shí)很簡(jiǎn)單,直接通過(guò) rawCnt & 1 就可以判斷,如果返回 0 表示 rawCnt 是一個(gè)偶數(shù),如果返回 1 表示 rawCnt 是一個(gè)奇數(shù)。
但是我們看到 Netty 在奇偶判斷條件的前面又加上了 rawCnt != 2 && rawCnt != 4 語(yǔ)句,這是干嘛的呢 ?
其實(shí) Netty 這里是為了盡量用性能更高的 == 運(yùn)算來(lái)代替 & 運(yùn)算,但又不可能用 == 運(yùn)算來(lái)枚舉出所有的偶數(shù)值(也沒這必要),所以只用 == 運(yùn)算來(lái)判斷在實(shí)際場(chǎng)景中經(jīng)常出現(xiàn)的引用計(jì)數(shù),一般經(jīng)常出現(xiàn)的引用計(jì)數(shù)值為 2 或者 4 , 也就是說(shuō) ByteBuf 在大部分場(chǎng)景下只會(huì)被引用 1 次或者 2 次,對(duì)于這種高頻出現(xiàn)的場(chǎng)景,Netty 用 == 運(yùn)算來(lái)針對(duì)性優(yōu)化,低頻出現(xiàn)的場(chǎng)景就回退到 & 運(yùn)算。
大部分性能優(yōu)化的套路都是相同的,我們通常不能一上來(lái)就奢求一個(gè)大而全的針對(duì)全局的優(yōu)化方案,這是不可能的,也是十分低效的。往往最有效的,可以立竿見影的優(yōu)化方案都是針對(duì)局部熱點(diǎn)進(jìn)行專門優(yōu)化。
對(duì)引用計(jì)數(shù)的設(shè)置也是一樣,都需要考慮奇偶的轉(zhuǎn)換,我們?cè)?setRefCnt 方法中指定的參數(shù) refCnt 表示邏輯上的引用計(jì)數(shù) —— 0, 1 , 2 , 3 ....,但要設(shè)置到 ByteBuf 時(shí),就需要對(duì)邏輯引用計(jì)數(shù)在乘以 2 ,讓它始終是一個(gè)偶數(shù)。
public final void setRefCnt(T instance, int refCnt) {
updater().set(instance, refCnt > 0 ? refCnt << 1 : 1); // overflow OK here
}
有了這些基礎(chǔ)之后,我們下面就來(lái)看一下在新版本的 retain 方法設(shè)計(jì)中,Netty 是如何解決 4.1.17.Final 版本存在的并發(fā)安全問(wèn)題。首先 Netty 對(duì)引用計(jì)數(shù)的奇偶設(shè)計(jì)對(duì)于用戶來(lái)說(shuō)是透明的。引用計(jì)數(shù)對(duì)于用戶來(lái)說(shuō)仍然是普通的自然數(shù) —— 0, 1 , 2 , 3 .... 。
所以每當(dāng)用戶調(diào)用 retain 方法試圖增加 ByteBuf 的引用計(jì)數(shù)時(shí),通常是指定邏輯增加步長(zhǎng) —— increment(用戶視角),而在具體的實(shí)現(xiàn)角度,Netty 會(huì)增加兩倍的 increment (rawIncrement)到 refCnt 字段中。
public final T retain(T instance) {
// 引用計(jì)數(shù)邏輯上是加 1 ,但實(shí)際上是加 2 (實(shí)現(xiàn)角度)
return retain0(instance, 1, 2);
}
public final T retain(T instance, int increment) {
// all changes to the raw count are 2x the "real" change - overflow is OK
// rawIncrement 始終是邏輯計(jì)數(shù) increment 的兩倍
int rawIncrement = checkPositive(increment, "increment") << 1;
// 將 rawIncrement 設(shè)置到 ByteBuf 的 refCnt 字段中
return retain0(instance, increment, rawIncrement);
}
// rawIncrement = increment << 1
// increment 表示引用計(jì)數(shù)的邏輯增長(zhǎng)步長(zhǎng)
// rawIncrement 表示引用計(jì)數(shù)的實(shí)際增長(zhǎng)步長(zhǎng)
private T retain0(T instance, final int increment, final int rawIncrement) {
// 先通過(guò) XADD 指令將 refCnt 的值加起來(lái)
int oldRef = updater().getAndAdd(instance, rawIncrement);
// 如果 oldRef 是一個(gè)奇數(shù),也就是 ByteBuf 已經(jīng)沒有引用了,拋出異常
if (oldRef != 2 && oldRef != 4 && (oldRef & 1) != 0) {
// 如果 oldRef 已經(jīng)是一個(gè)奇數(shù)了,無(wú)論多線程在這里怎么并發(fā) retain ,都是一個(gè)奇數(shù),這里都會(huì)拋出異常
throw new IllegalReferenceCountException(0, increment);
}
// don't pass 0!
// refCnt 不可能為 0 ,只能是 1
if ((oldRef <= 0 && oldRef + rawIncrement >= 0)
|| (oldRef >= 0 && oldRef + rawIncrement < oldRef)) {
// 如果 refCnt 字段已經(jīng)溢出,則進(jìn)行回退,并拋異常
updater().getAndAdd(instance, -rawIncrement);
throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
}
return instance;
}
首先新版本的 retain0 方法仍然保留了 4.1.17.Final 版本引入的 XADD 指令帶來(lái)的性能優(yōu)勢(shì),大致的處理邏輯也是類似的,一上來(lái)先通過(guò) getAndAdd 方法將 refCnt 增加 rawIncrement,對(duì)于 retain(T instance) 來(lái)說(shuō)這里直接加 2 。
然后判斷原來(lái)的引用計(jì)數(shù) oldRef 是否是一個(gè)奇數(shù),如果是一個(gè)奇數(shù),那么就表示 ByteBuf 已經(jīng)沒有任何引用了,邏輯引用計(jì)數(shù)早已經(jīng)為 0 了,那么就拋出 IllegalReferenceCountException。
在引用計(jì)數(shù)為奇數(shù)的情況下,無(wú)論多線程怎么對(duì) refCnt 并發(fā)加 2 ,refCnt 始終是一個(gè)奇數(shù),最終都會(huì)拋出異常。解決并發(fā)安全問(wèn)題的要點(diǎn)就在這里,一定要保證 retain 方法的并發(fā)執(zhí)行不能改變?cè)瓉?lái)的語(yǔ)義。
最后會(huì)判斷一下 refCnt 字段是否發(fā)生溢出,如果溢出,則進(jìn)行回退,并拋出異常。下面我們?nèi)匀灰灾暗牟l(fā)場(chǎng)景為例,用一個(gè)具體的例子,來(lái)回味一下奇偶設(shè)計(jì)的精妙之處。

現(xiàn)在線程 1 對(duì)一個(gè) refCnt 為 2 的 ByteBuf 執(zhí)行 release 方法,這時(shí) ByteBuf 的邏輯引用計(jì)數(shù)就為 0 了,對(duì)于一個(gè)沒有任何引用的 ByteBuf 來(lái)說(shuō),新版的設(shè)計(jì)中它的 refCnt 只能是一個(gè)奇數(shù),不能為 0 ,所以這里 Netty 會(huì)將 refCnt 設(shè)置為 1 。然后在步驟 2 中調(diào)用 deallocate 方法釋放 Native Memory。
線程 2 在步驟 1 和步驟 2 之間插入進(jìn)來(lái)對(duì) ByteBuf 并發(fā)執(zhí)行 retain 方法,這時(shí)線程 2 看到的 refCnt 是 1,然后通過(guò) getAndAdd 將 refCnt 加到了 3 ,仍然是一個(gè)奇數(shù),隨后拋出 IllegalReferenceCountException 異常。
線程 3 在步驟 1.1 和步驟 1.2 之間插入進(jìn)來(lái)再次對(duì) ByteBuf 并發(fā)執(zhí)行 retain 方法,這時(shí)線程 3 看到的 refCnt 是 3,然后通過(guò) getAndAdd 將 refCnt 加到了 5 ,還是一個(gè)奇數(shù),隨后拋出 IllegalReferenceCountException 異常。
這樣一來(lái)就保證了引用計(jì)數(shù)的并發(fā)語(yǔ)義 —— 只要一個(gè) ByteBuf 沒有任何引用的時(shí)候(refCnt = 1),其他線程無(wú)論怎么并發(fā)執(zhí)行 retain 方法都會(huì)得到一個(gè)異常。
但是引用計(jì)數(shù)并發(fā)語(yǔ)義的保證不能單單只靠 retain 方法,它還需要與 release 方法相互配合協(xié)作才可以,所以為了并發(fā)語(yǔ)義的保證 , release 方法的設(shè)計(jì)就不能使用性能更高的 XADD 指令,而是要回退到 CMPXCHG 指令來(lái)實(shí)現(xiàn)。
為什么這么說(shuō)呢 ?因?yàn)樾掳嬉糜?jì)數(shù)的設(shè)計(jì)采用的是奇偶實(shí)現(xiàn),refCnt 為偶數(shù)表示 ByteBuf 還有引用,refCnt 為奇數(shù)表示 ByteBuf 已經(jīng)沒有任何引用了,可以安全釋放 Native Memory 。對(duì)于一個(gè) refCnt 已經(jīng)為奇數(shù)的 ByteBuf 來(lái)說(shuō),無(wú)論多線程怎么并發(fā)執(zhí)行 retain 方法,得到的 refCnt 仍然是一個(gè)奇數(shù),最終都會(huì)拋出 IllegalReferenceCountException,這就是引用計(jì)數(shù)的并發(fā)語(yǔ)義 。
為了保證這一點(diǎn),就需要在每次調(diào)用 retain ,release 方法的時(shí)候,以偶數(shù)步長(zhǎng)來(lái)更新 refCnt,比如每一次調(diào)用 retain 方法就對(duì) refCnt 加 2 ,每一次調(diào)用 release 方法就對(duì) refCnt 減 2 。
但總有一個(gè)時(shí)刻,refCnt 會(huì)被減到 0 的對(duì)吧,在新版的奇偶設(shè)計(jì)中,refCnt 是不允許為 0 的,因?yàn)橐坏?refCnt 被減到了 0 ,多線程并發(fā)執(zhí)行 retain 之后,就會(huì)將 refCnt 再次加成了偶數(shù),這又會(huì)出現(xiàn)并發(fā)問(wèn)題。
而每一次調(diào)用 release 方法是對(duì) refCnt 減 2 ,如果我們采用 XADD 指令實(shí)現(xiàn) release 的話,回想一下 4.1.17.Final 版本中的設(shè)計(jì),它首先進(jìn)來(lái)是通過(guò) getAndAdd 方法對(duì) refCnt 減 2 ,這樣一來(lái),refCnt 就變成 0 了,就有并發(fā)安全問(wèn)題了。所以我們需要通過(guò) CMPXCHG 指令將 refCnt 更新為 1。
這里有的同學(xué)可能要問(wèn)了,那可不可以先進(jìn)行一下 if 判斷,如果 refCnt 減 2 之后變?yōu)?0 了,我們?cè)谕ㄟ^(guò) getAndAdd 方法將 refCnt 更新為 1 (減一個(gè)奇數(shù)),這樣一來(lái)不也可以利用上 XADD 指令的性能優(yōu)勢(shì)嗎 ?
答案是不行的,因?yàn)?if 判斷與 getAndAdd 更新這兩個(gè)操作之間仍然不是原子的,多線程可以在這個(gè)間隙仍然有并發(fā)執(zhí)行 retain 方法的可能,如下圖所示:

在線程 1 執(zhí)行 if 判斷和 getAndAdd 更新這兩個(gè)操作之間,線程 2 看到的 refCnt 其實(shí) 2 ,然后線程 2 會(huì)將 refCnt 加到 4 ,線程 3 緊接著會(huì)將 refCnt 增加到 6 ,在線程 2 和線程 3 看來(lái)這個(gè) ByteBuf 完全是正常的,但是線程 1 馬上就會(huì)釋放 Native Memory 了。
而且采用這種設(shè)計(jì)的話,一會(huì)通過(guò) getAndAdd 對(duì) refCnt 減一個(gè)奇數(shù),一會(huì)通過(guò) getAndAdd 對(duì) refCnt 加一個(gè)偶數(shù),這樣就把原本的奇偶設(shè)計(jì)搞亂掉了。
所以我們的設(shè)計(jì)目標(biāo)是一定要保證在 ByteBuf 沒有任何引用計(jì)數(shù)的時(shí)候,release 方法需要原子性的將 refCnt 更新為 1 。 因此必須采用 CMPXCHG 指令來(lái)實(shí)現(xiàn)而不能使用 XADD 指令。
再者說(shuō), CMPXCHG 指令是可以原子性的判斷當(dāng)前是否有并發(fā)情況的,如果有并發(fā)情況出現(xiàn),CAS 就會(huì)失敗,我們可以繼續(xù)重試。但 XADD 指令卻無(wú)法原子性的判斷是否有并發(fā)情況,因?yàn)樗看味际窍雀?,后判斷并發(fā),這就不是原子的了。這一點(diǎn),在下面的源碼實(shí)現(xiàn)中會(huì)體現(xiàn)的特別明顯。
7. 盡量避免內(nèi)存屏障的開銷
public final boolean release(T instance) {
// 第一次嘗試采用 unSafe nonVolatile 的方式讀取 refCnf 的值
int rawCnt = nonVolatileRawCnt(instance);
// 如果邏輯引用計(jì)數(shù)被減到 0 了,那么就通過(guò) tryFinalRelease0 使用 CAS 將 refCnf 更新為 1
// CAS 失敗的話,則通過(guò) retryRelease0 進(jìn)行重試
// 如果邏輯引用計(jì)數(shù)不為 0 ,則通過(guò) nonFinalRelease0 將 refCnf 減 2
return rawCnt == 2 ? tryFinalRelease0(instance, 2) || retryRelease0(instance, 1)
: nonFinalRelease0(instance, 1, rawCnt, toLiveRealRefCnt(rawCnt, 1));
}
這里有一個(gè)小的細(xì)節(jié)再次體現(xiàn)出 Netty 對(duì)于性能的極致追求,refCnt 字段在 ByteBuf 中被 Netty 申明為一個(gè) volatile 字段。
private volatile int refCnt = updater.initialValue();
我們對(duì) refCnt 的普通讀寫都是要走內(nèi)存屏障的,但 Netty 在 release 方法中首次讀取 refCnt 的值是采用 nonVolatile 的方式,不走內(nèi)存屏障,直接讀取 cache line,避免了屏障開銷。
private int nonVolatileRawCnt(T instance) {
// 獲取 REFCNT_FIELD_OFFSET
final long offset = unsafeOffset();
// 通過(guò) UnSafe 的方式來(lái)訪問(wèn) refCnt , 避免內(nèi)存屏障的開銷
return offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance);
}
那有的同學(xué)可能要問(wèn)了,如果讀取 refCnt 的時(shí)候不走內(nèi)存屏障的話,讀取到的 refCnt 不就可能是一個(gè)錯(cuò)誤的值嗎 ?
事實(shí)上確實(shí)是這樣的,但 Netty 不 care , 讀到一個(gè)錯(cuò)誤的值也無(wú)所謂,因?yàn)檫@里的引用計(jì)數(shù)采用了奇偶設(shè)計(jì),我們?cè)诘谝淮巫x取引用計(jì)數(shù)的時(shí)候并不需要讀取到一個(gè)精確的值,既然這樣我們可以直接通過(guò) UnSafe 來(lái)讀取,還能剩下一筆內(nèi)存屏障的開銷。
那為什么不需要一個(gè)精確的值呢 ?因?yàn)槿绻瓉?lái)的 refCnt 是一個(gè)奇數(shù),那無(wú)論多線程怎么并發(fā) retain ,最終得到的還是一個(gè)奇數(shù),我們這里只需要知道 refCnt 是一個(gè)奇數(shù)就可以直接拋 IllegalReferenceCountException 了。具體讀到的是一個(gè) 3 還是一個(gè) 5 其實(shí)都無(wú)所謂。
那如果原來(lái)的 refCnt 是一個(gè)偶數(shù)呢 ?其實(shí)也無(wú)所謂,我們可能讀到一個(gè)正確的值也可能讀到一個(gè)錯(cuò)誤的值,如果恰好讀到一個(gè)正確的值,那更好。如果讀取到一個(gè)錯(cuò)誤的值,也無(wú)所謂,因?yàn)槲覀兒竺媸怯?CAS 進(jìn)行更新,這樣的話 CAS 就會(huì)更新失敗,我們只需要在一下輪 for 循環(huán)中更新正確就可以了。
如果讀取到的 refCnt 恰好是 2 ,那就意味著本次 release 之后,ByteBuf 的邏輯引用計(jì)數(shù)就為 0 了,Netty 會(huì)通過(guò) CAS 將 refCnt 更新為 1 。
private boolean tryFinalRelease0(T instance, int expectRawCnt) {
return updater().compareAndSet(instance, expectRawCnt, 1); // any odd number will work
}
如果 CAS 更新失敗,則表示此時(shí)有多線程可能并發(fā)對(duì) ByteBuf 執(zhí)行 retain 方法,邏輯引用計(jì)數(shù)此時(shí)可能就不為 0 了,針對(duì)這種并發(fā)情況,Netty 會(huì)在 retryRelease0 方法中進(jìn)行重試,將 refCnt 減 2 。
private boolean retryRelease0(T instance, int decrement) {
for (;;) {
// 采用 Volatile 的方式讀取 refCnt
int rawCnt = updater().get(instance),
// 獲取邏輯引用計(jì)數(shù),如果 refCnt 已經(jīng)變?yōu)槠鏀?shù),則拋出異常
realCnt = toLiveRealRefCnt(rawCnt, decrement);
// 如果執(zhí)行完本次 release , 邏輯引用計(jì)數(shù)為 0
if (decrement == realCnt) {
// CAS 將 refCnt 更新為 1
if (tryFinalRelease0(instance, rawCnt)) {
return true;
}
} else if (decrement < realCnt) {
// 原來(lái)的邏輯引用計(jì)數(shù) realCnt 大于 1(decrement)
// 則通過(guò) CAS 將 refCnt 減 2
if (updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
return false;
}
} else {
// refCnt 字段如果發(fā)生溢出,則拋出異常
throw new IllegalReferenceCountException(realCnt, -decrement);
}
// CAS 失敗之后調(diào)用 yield
// 減少無(wú)畏的競(jìng)爭(zhēng),否則所有線程在高并發(fā)情況下都在這里 CAS 失敗
Thread.yield();
}
}
從 retryRelease0 方法的實(shí)現(xiàn)中我們可以看出,CAS 是可以原子性的探測(cè)到是否有并發(fā)情況出現(xiàn)的,如果有并發(fā)情況,這里的所有 CAS 都會(huì)失敗,隨后會(huì)在下一輪 for 循環(huán)中將正確的值更新到 refCnt 中。這一點(diǎn) ,XADD 指令是做不到的。
如果在進(jìn)入 release 方法后,第一次讀取的 refCnt 不是 2 ,那么就不能走上面的 tryFinalRelease0 邏輯,而是在 nonFinalRelease0 中通過(guò) CAS 將 refCnt 的值減 2 。
private boolean nonFinalRelease0(T instance, int decrement, int rawCnt, int realCnt) {
if (decrement < realCnt
&& updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
// ByteBuf 的 rawCnt 減少 2 * decrement
return false;
}
// CAS 失敗則一直重試,如果引用計(jì)數(shù)已經(jīng)為 0 ,那么拋出異常,不能再次 release
return retryRelease0(instance, decrement);
}
總結(jié)
到這里,Netty 對(duì)引用計(jì)數(shù)的精彩設(shè)計(jì),筆者就為大家完整的剖析完了,一共有四處非常精彩的優(yōu)化設(shè)計(jì),我們總結(jié)如下:
使用性能更優(yōu)的 XADD 指令來(lái)替換 CMPXCHG 指令。
引用計(jì)數(shù)采用了奇偶設(shè)計(jì),保證了并發(fā)語(yǔ)義。
采用性能更優(yōu)的
==運(yùn)算來(lái)替換&運(yùn)算。能不走內(nèi)存屏障就盡量不走內(nèi)存屏障。