緩存過期算法
- LFU(Least Frequently Used祝钢,最近最不常用)
根據(jù)最近使用的頻率來淘汰頻率低的數(shù)據(jù)。核心思想是:如果數(shù)據(jù)過去多次被訪問若厚,那么將來被訪問的概率也高拦英。該算法需要維護(hù)數(shù)據(jù)的訪問頻率,開銷很大测秸。此外不適合應(yīng)對(duì)突發(fā)性熱點(diǎn)訪問場(chǎng)景疤估。 - LRU(Least recently used,最近最少使用)
根據(jù)最近訪問記錄淘汰最近很少被訪問的數(shù)據(jù)霎冯。核心思想是:如果數(shù)據(jù)最近被訪問過铃拇,那么將來被訪問的幾率也更高。缺點(diǎn)在于偶發(fā)性的批量操作會(huì)降低命中率沈撞。guavacache使用的就是此算法慷荔。 -
TinyLFU
輕量LFU算法,相比較LFU关串,使用更少的內(nèi)存來保存訪問頻率拧廊。TinyLFU保存了近期的訪問頻率,而不是整個(gè)生命周期的訪問頻率晋修,所以可以很好的應(yīng)對(duì)突發(fā)性熱點(diǎn)場(chǎng)景吧碾。這些訪問記錄會(huì)作為一個(gè)過濾頻率,當(dāng)新加入的數(shù)據(jù)訪問頻率比要淘汰的數(shù)據(jù)訪問頻率才加入緩存墓卦。
TinyLFU 通過 Count-Min Sketch 算法來記錄頻率信息倦春,它占用空間小且誤報(bào)率低。但在應(yīng)對(duì)稀疏的突發(fā)性訪問量大的場(chǎng)景落剪,將很難保存這類元素睁本,因?yàn)榭赡軣o法在短時(shí)間內(nèi)積累足夠的訪問頻率,從而被過濾器過濾掉忠怖。
-
W-TinyLFU
對(duì)TinyLFU在稀疏突發(fā)性訪問大的場(chǎng)景做了優(yōu)化呢堰,W-TinyLFU 將新記錄暫時(shí)放入 Window Cache 里面,只有通過 TinLFU 考察才能進(jìn)入 Main Cache凡泣。
其中過濾器使用了Count-Min Sketch算法(一種布隆過濾器的變種)實(shí)現(xiàn)枉疼,即根據(jù)不同的hash算法創(chuàng)建不同的數(shù)組皮假,針對(duì)每一個(gè)數(shù)據(jù)進(jìn)行多次hash,并在該hash算法的對(duì)應(yīng)數(shù)組hash索引位置上+1骂维,由于hash算法存在沖突惹资,那么在最后取計(jì)數(shù)的時(shí)候,取所有數(shù)組中最小的值即可航闺。
Caffeine簡(jiǎn)介
相對(duì)于guavacache褪测,caffeine采用更好的過期策略W-TinyLFU,并通過RingBuffer緩存訪問信息潦刃,再批量異步處理侮措;此外caffeine底層直接使用jdk1.8的ConcurrentHashMap(因此caffeine只能在1.8上使用),比1.7的ConcurrentHashMap要快不少(guavacache底層實(shí)現(xiàn)類似于1.7的ConcurrentHashMap)乖杠,官方號(hào)稱其性能接近jdk1.8的ConcurrentHashMap萝毛。下面是官方的測(cè)試對(duì)比圖:
caffeine使用和guava差不多,因此切換過來成本較低滑黔。
讀寫數(shù)據(jù)結(jié)構(gòu)
不同于guavacache采用accessQueue、recencyQueue环揽、writeQueue隊(duì)列來記錄讀寫操作略荡。caffeine采用的是readBuffer和writeBuffer。
readBuffer
采用多個(gè) RingBuffer(striped ring buffer 條帶環(huán)形緩沖歉胶,有損)汛兜,通過線程 id 哈希到對(duì)應(yīng)的RingBuffer。當(dāng)一個(gè)RingBuffer滿了后通今,后續(xù)的寫入會(huì)丟棄直到這個(gè)RingBuffer可用粥谬。
當(dāng)讀命中后,會(huì)將數(shù)據(jù)寫入RingBuffer辫塌,這個(gè)寫入操作性能很高漏策。然后由線程池(默認(rèn)forkjoin或自定義)異步消費(fèi)RingBuffer,將數(shù)據(jù)加入到數(shù)據(jù)淘汰隊(duì)列中臼氨。
writeBuffer
MpscGrowableArrayQueue實(shí)現(xiàn)掺喻,和JCTools中的原理差不多。寫和讀操作不一樣储矩,讀多寫少感耙,且不允許有損(丟失數(shù)據(jù)),mpsc可參考【netty學(xué)習(xí)筆記十七】Mpsc高性能無鎖隊(duì)列持隧。
自定義過期策略
caffeine除了支持expireAfterAccess和expireAfterWrite即硼,還支持expireAfter,即根據(jù)key定義不同的過期時(shí)間屡拨。這里的實(shí)現(xiàn)是用時(shí)間輪只酥,可參考【netty學(xué)習(xí)筆記十八】netty時(shí)間輪褥实。
源碼簡(jiǎn)析
直接看get方法,最終會(huì)調(diào)computeIfAbsent方法
public @Nullable V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
boolean recordStats, boolean recordLoad) {
long now = expirationTicker().read();
// 這里的data就是ConcurrentHashMap
Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
if (node != null) {
V value = node.getValue();
//如果key已存在且沒過期(這里過期都是exprire開頭的屬性)层皱,則返回value
if ((value != null) && !hasExpired(node, now)) {
if (!isComputingAsync(node)) {
tryExpireAfterRead(node, key, value, expiry(), now);
setAccessTime(node, now);
}
afterRead(node, now, /* recordHit */ recordStats);
return value;
}
}
if (recordStats) {
mappingFunction = statsAware(mappingFunction, recordLoad);
}
Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
}
/** Returns the current value from a computeIfAbsent invocation. */
@Nullable V doComputeIfAbsent(K key, Object keyRef,
Function<? super K, ? extends V> mappingFunction, long[/* 1 */] now, boolean recordStats) {
@SuppressWarnings("unchecked")
V[] oldValue = (V[]) new Object[1];
@SuppressWarnings("unchecked")
V[] newValue = (V[]) new Object[1];
@SuppressWarnings("unchecked")
K[] nodeKey = (K[]) new Object[1];
@SuppressWarnings({"unchecked", "rawtypes"})
Node<K, V>[] removed = new Node[1];
int[] weight = new int[2]; // old, new
RemovalCause[] cause = new RemovalCause[1];
Node<K, V> node = data.compute(keyRef, (k, n) -> {
// n!=null表示數(shù)據(jù)是過期的
if (n == null) {
// 這里最終調(diào)用自定義加載數(shù)據(jù)方法那里
newValue[0] = mappingFunction.apply(key);
if (newValue[0] == null) {
return null;
}
now[0] = expirationTicker().read();
weight[1] = weigher.weigh(key, newValue[0]);
n = nodeFactory.newNode(key, keyReferenceQueue(),
newValue[0], valueReferenceQueue(), weight[1], now[0]);
setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
return n;
}
// 對(duì)節(jié)點(diǎn)加鎖性锭,若n過期了, 其他線程會(huì)等待n更新完畢
synchronized (n) {
nodeKey[0] = n.getKey();
weight[0] = n.getWeight();
oldValue[0] = n.getValue();
if ((nodeKey[0] == null) || (oldValue[0] == null)) {
cause[0] = RemovalCause.COLLECTED;
} else if (hasExpired(n, now[0])) {
cause[0] = RemovalCause.EXPIRED;
} else {
return n;
}
//刪除數(shù)據(jù)并重新加載
writer.delete(nodeKey[0], oldValue[0], cause[0]);
newValue[0] = mappingFunction.apply(key);
if (newValue[0] == null) {
removed[0] = n;
n.retire();
return null;
}
weight[1] = weigher.weigh(key, newValue[0]);
n.setValue(newValue[0], valueReferenceQueue());
n.setWeight(weight[1]);
now[0] = expirationTicker().read();
setVariableTime(n, expireAfterCreate(key, newValue[0], expiry(), now[0]));
setAccessTime(n, now[0]);
setWriteTime(n, now[0]);
return n;
}
});
if (node == null) {
if (removed[0] != null) {
afterWrite(new RemovalTask(removed[0]));
}
return null;
}
if (cause[0] != null) {
// 舊值移除通知
if (hasRemovalListener()) {
notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
}
statsCounter().recordEviction(weight[0]);
}
// 新值還未計(jì)算完叫胖?
if (newValue[0] == null) {
if (!isComputingAsync(node)) {
tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
setAccessTime(node, now[0]);
}
afterRead(node, now[0], /* recordHit */ recordStats);
return oldValue[0];
}
//記錄寫操作
if ((oldValue[0] == null) && (cause[0] == null)) {
afterWrite(new AddTask(node, weight[1]));
} else {
int weightedDifference = (weight[1] - weight[0]);
afterWrite(new UpdateTask(node, weightedDifference));
}
//返回新值
return newValue[0];
}
refreshAfterWrite和expireAfterWrite區(qū)別
和guava有點(diǎn)不一樣草冈,refreshAfterWrite是過期了直接返回舊值,然后通過異步線程池進(jìn)行刷新(默認(rèn)線程池為forkjoin)瓮增。而expireAfterWrite是一樣的怎棱,都是加載新值,其他線程需要等待绷跑。
refreshAfterWrite在BoundedLocalCache#afterRead -> refreshIfNeeded方法:
void refreshIfNeeded(Node<K, V> node, long now) {
if (!refreshAfterWrite()) {
return;
}
K key;
V oldValue;
long oldWriteTime = node.getWriteTime();
long refreshWriteTime = (now + ASYNC_EXPIRY);
if (((now - oldWriteTime) > refreshAfterWriteNanos())
&& ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
&& node.casWriteTime(oldWriteTime, refreshWriteTime)) {
try {
CompletableFuture<V> refreshFuture;
long startTime = statsTicker().read();
//是否異步
if (isAsync) {
@SuppressWarnings("unchecked")
CompletableFuture<V> future = (CompletableFuture<V>) oldValue;
if (Async.isReady(future)) {
@SuppressWarnings("NullAway")
CompletableFuture<V> refresh = future.thenCompose(value ->
cacheLoader.asyncReload(key, value, executor));
refreshFuture = refresh;
} else {
// no-op if load is pending
node.casWriteTime(refreshWriteTime, oldWriteTime);
return;
}
} else {
@SuppressWarnings("NullAway")
// 異常加載新值
CompletableFuture<V> refresh = cacheLoader.asyncReload(key, oldValue, executor);
refreshFuture = refresh;
}
refreshFuture.whenComplete((newValue, error) -> {
long loadTime = statsTicker().read() - startTime;
if (error != null) {
logger.log(Level.WARNING, "Exception thrown during refresh", error);
node.casWriteTime(refreshWriteTime, oldWriteTime);
statsCounter().recordLoadFailure(loadTime);
return;
}
@SuppressWarnings("unchecked")
V value = (isAsync && (newValue != null)) ? (V) refreshFuture : newValue;
boolean[] discard = new boolean[1];
compute(key, (k, currentValue) -> {
if (currentValue == null) {
return value;
} else if ((currentValue == oldValue) && (node.getWriteTime() == refreshWriteTime)) {
return value;
}
discard[0] = true;
return currentValue;
}, /* recordMiss */ false, /* recordLoad */ false, /* recordLoadFailure */ true);
if (discard[0] && hasRemovalListener()) {
notifyRemoval(key, value, RemovalCause.REPLACED);
}
if (newValue == null) {
statsCounter().recordLoadFailure(loadTime);
} else {
statsCounter().recordLoadSuccess(loadTime);
}
});
} catch (Throwable t) {
node.casWriteTime(refreshWriteTime, oldWriteTime);
logger.log(Level.SEVERE, "Exception thrown when submitting refresh task", t);
}
}
}
refreshAfterWrite異步加載過程如上拳恋,expireAfterWrite會(huì)在hasExpired就會(huì)判斷,如果過期就調(diào)用doComputeIfAbsent加載新值砸捏。
boolean hasExpired(Node<K, V> node, long now) {
return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
| (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
| (expiresVariable() && (now - node.getVariableTime() >= 0));
}
Count–Min Sketch算法實(shí)現(xiàn)
上文寫到caffeine采用W-TinyLFU實(shí)現(xiàn)淘汰策略谬运,其中過濾器這塊使用了Count–Min Sketch算法。caffeine這塊的實(shí)現(xiàn)在FrequencySketch類中垦藏,在實(shí)現(xiàn)中做了一些優(yōu)化梆暖。
FrequencySketch使用一個(gè)long數(shù)組來記錄訪問頻率,數(shù)組大小為最接近緩存大小且比其大的2的冪數(shù)掂骏。在FrequencySketch中轰驳,認(rèn)為最大的訪問頻率是15,換成二進(jìn)制則是4位弟灼,那一個(gè)long理論上可以放16種算法级解。但caffeine將long分為16等份,每份4bit用來存儲(chǔ)對(duì)應(yīng)的頻率田绑。long結(jié)構(gòu)如下:
我們?cè)倏纯刺砑右淮卧L問頻率的代碼:
public void increment(@NonNull E e) {
if (isNotInitialized()) {
return;
}
// 和jdk的hashmap一樣勤哗,這里的spread操作會(huì)讓hash更均勻
int hash = spread(e.hashCode());
// 獲取一個(gè)小于16的數(shù),即判斷在long里那個(gè)等份
int start = (hash & 3) << 2;
// 設(shè)計(jì)了4種hash方法(對(duì)應(yīng)不同的種子)掩驱,分別計(jì)算4個(gè)不同的table(long數(shù)組)下標(biāo)
int index0 = indexOf(hash, 0);
int index1 = indexOf(hash, 1);
int index2 = indexOf(hash, 2);
int index3 = indexOf(hash, 3);
// 將table[index]俺陋、table[index+1]、table[index+2]昙篙、table[index+3]對(duì)應(yīng)的等分追加1
boolean added = incrementAt(index0, start);
added |= incrementAt(index1, start + 1);
added |= incrementAt(index2, start + 2);
added |= incrementAt(index3, start + 3);
//處理頻率很高但不經(jīng)常使用的數(shù)據(jù)
if (added && (++size == sampleSize)) {
reset();
}
}
boolean incrementAt(int i, int j) {
//j為16等份的下標(biāo)腊状,offset即為64等份的下標(biāo)
int offset = j << 2;
// mask用來判斷是否等于15
long mask = (0xfL << offset);
if ((table[i] & mask) != mask) {
// 不等于15則追加1
table[i] += (1L << offset);
return true;
}
return false;
}
數(shù)據(jù)保新
如果有些數(shù)據(jù)頻率很高,但不經(jīng)常使用怎么辦苔可,總不能一直放在long數(shù)組中吧缴挖。Caffeine有個(gè)機(jī)制,當(dāng)所有數(shù)據(jù)的統(tǒng)計(jì)頻率數(shù)達(dá)到某一個(gè)閾值(默認(rèn)為maximum的10倍)焚辅,則對(duì)所有數(shù)的頻率減半映屋。
if (added && (++size == sampleSize)) {
reset();
}
/** Reduces every counter by half of its original value. */
void reset() {
int count = 0;
for (int i = 0; i < table.length; i++) {
count += Long.bitCount(table[i] & ONE_MASK);
table[i] = (table[i] >>> 1) & RESET_MASK;
}
size = (size >>> 1) - (count >>> 2);
}
Window
上面提到TinyLFU在面對(duì)突發(fā)性的稀疏流量時(shí)表現(xiàn)很差苟鸯,新數(shù)據(jù)很難積累到足夠多的頻率來通過過濾器。而caffeine在此基礎(chǔ)上做了優(yōu)化棚点,引入Window Tiny LFU(W-TinyLFU)早处。
當(dāng)window區(qū)數(shù)據(jù)滿了,就會(huì)根據(jù)LRU把數(shù)據(jù)candidate放入probation區(qū)瘫析,如果probation也滿了砌梆,則跟probation數(shù)據(jù)進(jìn)行pk,輸?shù)谋惶蕴?br>
caffeine默認(rèn)配置為window容量占1%贬循,剩余的80%為Protected咸包,20%為probation(實(shí)驗(yàn)測(cè)試這樣配置效果最好),實(shí)際運(yùn)行時(shí)會(huì)動(dòng)態(tài)調(diào)整杖虾。
驅(qū)逐代碼如下:
/** Evicts entries if the cache exceeds the maximum. */
@GuardedBy("evictionLock")
void evictEntries() {
if (!evicts()) {
return;
}
//淘汰window區(qū)的記錄
int candidates = evictFromWindow();
//淘汰main區(qū)的記錄
evictFromMain(candidates);
}
/**
* Evicts entries from the window space into the main space while the window size exceeds a
* maximum.
*
* @return the number of candidate entries evicted from the window space
*/
@GuardedBy("evictionLock")
int evictFromWindow() {
int candidates = 0;
//獲取window queue的頭部節(jié)點(diǎn)
Node<K, V> node = accessOrderWindowDeque().peek();
//若超過window最大限制烂瘫,則處理
while (windowWeightedSize() > windowMaximum()) {
// The pending operations will adjust the size to reflect the correct weight
if (node == null) {
break;
}
Node<K, V> next = node.getNextInAccessOrder();
if (node.getWeight() != 0) {
node.makeMainProbation();
//從window區(qū)移除并加入Probation
accessOrderWindowDeque().remove(node);
accessOrderProbationDeque().add(node);
candidates++;
//調(diào)整size
setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
}
node = next;
}
return candidates;
}
@GuardedBy("evictionLock")
void evictFromMain(int candidates) {
int victimQueue = PROBATION;
//分別獲取第一個(gè)和最后個(gè)
Node<K, V> victim = accessOrderProbationDeque().peekFirst();
Node<K, V> candidate = accessOrderProbationDeque().peekLast();
// 當(dāng)cache容量不夠時(shí)處理
while (weightedSize() > maximum()) {
// Stop trying to evict candidates and always prefer the victim
if (candidates == 0) {
candidate = null;
}
// Try evicting from the protected and window queues
// 嘗試從protected和window區(qū)獲取victim數(shù)據(jù)
if ((candidate == null) && (victim == null)) {
if (victimQueue == PROBATION) {
victim = accessOrderProtectedDeque().peekFirst();
victimQueue = PROTECTED;
continue;
} else if (victimQueue == PROTECTED) {
victim = accessOrderWindowDeque().peekFirst();
victimQueue = WINDOW;
continue;
}
// The pending operations will adjust the size to reflect the correct weight
break;
}
// Skip over entries with zero weight
if ((victim != null) && (victim.getPolicyWeight() == 0)) {
victim = victim.getNextInAccessOrder();
continue;
} else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
candidate = candidate.getPreviousInAccessOrder();
candidates--;
continue;
}
// Evict immediately if only one of the entries is present
if (victim == null) {
@SuppressWarnings("NullAway")
Node<K, V> previous = candidate.getPreviousInAccessOrder();
Node<K, V> evict = candidate;
candidate = previous;
candidates--;
evictEntry(evict, RemovalCause.SIZE, 0L);
continue;
} else if (candidate == null) {
Node<K, V> evict = victim;
victim = victim.getNextInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
continue;
}
// Evict immediately if an entry was collected
K victimKey = victim.getKey();
K candidateKey = candidate.getKey();
if (victimKey == null) {
@NonNull Node<K, V> evict = victim;
victim = victim.getNextInAccessOrder();
evictEntry(evict, RemovalCause.COLLECTED, 0L);
continue;
} else if (candidateKey == null) {
candidates--;
@NonNull Node<K, V> evict = candidate;
candidate = candidate.getPreviousInAccessOrder();
evictEntry(evict, RemovalCause.COLLECTED, 0L);
continue;
}
// Evict immediately if the candidate's weight exceeds the maximum
// weight太大的節(jié)點(diǎn)直接驅(qū)逐
if (candidate.getPolicyWeight() > maximum()) {
candidates--;
Node<K, V> evict = candidate;
candidate = candidate.getPreviousInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
continue;
}
// Evict the entry with the lowest frequency
candidates--;
// admit根據(jù)頻率記錄來比較
if (admit(candidateKey, victimKey)) {
Node<K, V> evict = victim;
victim = victim.getNextInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
candidate = candidate.getPreviousInAccessOrder();
} else {
Node<K, V> evict = candidate;
candidate = candidate.getPreviousInAccessOrder();
evictEntry(evict, RemovalCause.SIZE, 0L);
}
}
}
//頻率比較
@GuardedBy("evictionLock")
boolean admit(K candidateKey, K victimKey) {
int victimFreq = frequencySketch().frequency(victimKey);
int candidateFreq = frequencySketch().frequency(candidateKey);
//誰大誰贏
if (candidateFreq > victimFreq) {
return true;
//相等,則candidateFreq <= 5算輸奇适。這里有考慮衰減的情況坟比,怕candidate利用這個(gè)規(guī)律淘汰老數(shù)據(jù),主要是提高命中率嚷往。
} else if (candidateFreq <= 5) {
// The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack
// exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm
// candidate reduces the number of random acceptances to minimize the impact on the hit rate.
return false;
}
// 相等且candidateFreq>5,則隨機(jī)淘汰一個(gè)
int random = ThreadLocalRandom.current().nextInt();
return ((random & 127) == 0);
}
高性能讀寫操作
緩存對(duì)數(shù)據(jù)進(jìn)行處理(讀温算、寫)后,都會(huì)伴隨一些額外的操作间影,如:
- 判斷數(shù)據(jù)是否過期;
- 統(tǒng)計(jì)頻率茄茁;
- 記錄讀寫
- 統(tǒng)計(jì)命中率等
在guava中魂贬,讀寫操作和這些額外的操作一起進(jìn)行。caffeine借鑒了WAL思想裙顽,執(zhí)行讀寫操作后付燥,將操作記錄記載緩沖區(qū),后面再異步處理愈犹,提高了性能键科。
ReadBuffer
每次讀命中后,會(huì)執(zhí)行afterRead:
void afterRead(Node<K, V> node, long now, boolean recordHit) {
if (recordHit) {
statsCounter().recordHits(1);
}
// 將數(shù)據(jù)加入ReadBuffer
boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
if (shouldDrainBuffers(delayable)) {
scheduleDrainBuffers();
}
refreshIfNeeded(node, now);
}
// 首先會(huì)進(jìn)入BoundedBuffer的父類StripedBuffer
public int offer(E e) {
int mask;
int result = 0;
Buffer<E> buffer;
boolean uncontended = true;
Buffer<E>[] buffers = table;
if ((buffers == null)
|| (mask = buffers.length - 1) < 0
//根據(jù)線程id判斷在哪個(gè)buffer
|| (buffer = buffers[getProbe() & mask]) == null
|| !(uncontended = ((result = buffer.offer(e)) != Buffer.FAILED))) {
expandOrRetry(e, uncontended);
}
return result;
}
public int offer(E e) {
long head = readCounter;
long tail = relaxedWriteCounter();
//獲取buffer大小漩怎,超過則加入失敗勋颖,即允許有數(shù)據(jù)丟失
long size = (tail - head);
if (size >= SPACED_SIZE) {
return Buffer.FULL;
}
// cas操作追加16
if (casWriteCounter(tail, tail + OFFSET)) {
//求余獲取下標(biāo)
int index = (int) (tail & SPACED_MASK);
buffer.lazySet(index, e);
return Buffer.SUCCESS;
}
return Buffer.FAILED;
}
這里要注意下,ringbuffer默認(rèn)為16勋锤,而其數(shù)組大小是256饭玲,這里是假設(shè)引用大小為4字節(jié)(ringbuffer存的是引用),緩存行大小為64叁执。所以這里每個(gè)緩存行只存一個(gè)數(shù)據(jù)茄厘,所以cas操作追加16矮冬,即數(shù)組中每16個(gè)元素只有一個(gè)有效存儲(chǔ),空間換時(shí)間次哈。
參考
https://albenw.github.io/posts/a4ae1aa2/
https://zhouxinghang.github.io/caffeine.html