万字详解本地缓存之王 Caffeine

//window区的最大限制

long windowMaximum;

//window区当前的个数

long windowWeightedSize;

//protected区的最大限制

long mainProtectedMaximum;

//protected区当前的个数

long mainProtectedWeightedSize;

//下一次需要调整的大小(还需要进一步计算)

double stepSize;

//window区需要调整的大小

long adjustment;

//命中计数

int hitsInSample;

//不命中的计数

int missesInSample;

//上一次的缓存命中率

double previousSampleHitRate;

final FrequencySketch sketch;

//window区的LRU queue(FIFO)

final AccessOrderDeque<Node<K, V>> accessOrderWindowDeque;

//probation区的LRU queue(FIFO)

final AccessOrderDeque<Node<K, V>> accessOrderProbationDeque;

//protected区的LRU queue(FIFO)

final AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque;

以及默认比例设置(意思看注释)

/** The initial percent of the maximum weighted capacity dedicated to the main space. */

static final double PERCENT_MAIN = 0.99d;

/** The percent of the maximum weighted capacity dedicated to the main’s protected space. */

static final double PERCENT_MAIN_PROTECTED = 0.80d;

/** The difference in hit rates that restarts the climber. */

static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;

/** The percent of the total size to adapt the window by. */

static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;

/** The rate to decrease the step size to adapt by. */

static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;

/** The maximum number of entries that can be transfered between queues. */

重点来了,evictEntriesclimb方法:

/** Evicts entries if the cache exceeds the maximum. */

@GuardedBy(“evictionLock”)

void evictEntries() {

if (!evicts()) {

return;

}

//淘汰window区的记录

int candidates = evictFromWindow();

//淘汰Main区的记录

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

*/

//根据W-TinyLFU,新的数据都会无条件的加到admission window

//但是window是有大小限制,所以要“定期”做一下“维护”

@GuardedBy(“evictionLock”)

int evictFromWindow() {

int candidates = 0;

//查看window queue的头部节点

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定位在probation区

node.makeMainProbation();

//从window区去掉

accessOrderWindowDeque().remove(node);

//加入到probation queue,相当于把节点移动到probation区(晋升了)

accessOrderProbationDeque().add(node);

candidates++;

//因为移除了一个节点,所以需要调整window的size

setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());

}

//处理下一个节点

node = next;

}

return candidates;

}

evictFromMain方法:

/**

* Evicts entries from the main space if the cache exceeds the maximum capacity. The main space

* determines whether admitting an entry (coming from the window space) is preferable to retaining

* the eviction policy’s victim. This is decision is made using a frequency filter so that the

* least frequently used entry is removed.

* The window space candidates were previously placed in the MRU position and the eviction

* policy’s victim is at the LRU position. The two ends of the queue are evaluated while an

* eviction is required. The number of remaining candidates is provided and decremented on

* eviction, so that when there are no more candidates the victim is evicted.

* @param candidates the number of candidate entries evicted from the window space

*/

//根据W-TinyLFU,从window晋升过来的要跟probation区的进行“PK”,胜者才能留下

@GuardedBy(“evictionLock”)

void evictFromMain(int candidates) {

int victimQueue = PROBATION;

//victim是probation queue的头部

Node<K, V> victim = accessOrderProbationDeque().peekFirst();

//candidate是probation queue的尾部,也就是刚从window晋升来的

Node<K, V> candidate = accessOrderProbationDeque().peekLast();

//当cache不够容量时才做处理

while (weightedSize() > maximum()) {

// Stop trying to evict candidates and always prefer the victim

if (candidates == 0) {

candidate = null;

}

//对candidate为null且victim为bull的处理

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;

}

//对节点的weight为0的处理

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;

}

//放不下的节点直接处理掉

if (candidate.getPolicyWeight() > maximum()) {

candidates–;

Node<K, V> evict = candidate;

candidate = candidate.getPreviousInAccessOrder();

evictEntry(evict, RemovalCause.SIZE, 0L);

continue;

}

//根据节点的统计频率frequency来做比较,看看要处理掉victim还是candidate

//admit是具体的比较规则,看下面

candidates–;

//如果candidate胜出则淘汰victim

if (admit(candidateKey, victimKey)) {

Node<K, V> evict = victim;

victim = victim.getNextInAccessOrder();

evictEntry(evict, RemovalCause.SIZE, 0L);

candidate = candidate.getPreviousInAccessOrder();

} else {

//如果是victim胜出,则淘汰candidate

Node<K, V> evict = candidate;

candidate = candidate.getPreviousInAccessOrder();

evictEntry(evict, RemovalCause.SIZE, 0L);

}

}

}

/**

* Determines if the candidate should be accepted into the main space, as determined by its

* frequency relative to the victim. A small amount of randomness is used to protect against hash

* collision attacks, where the victim’s frequency is artificially raised so that no new entries

* are admitted.

* @param candidateKey the key for the entry being proposed for long term retention

* @param victimKey the key for the entry chosen by the eviction policy for replacement

* @return if the candidate should be admitted and the victim ejected

*/

@GuardedBy(“evictionLock”)

boolean admit(K candidateKey, K victimKey) {

//分别获取victim和candidate的统计频率

//frequency这个方法的原理和实现上面已经解释了

int victimFreq = frequencySketch().frequency(victimKey);

int candidateFreq = frequencySketch().frequency(candidateKey);

//谁大谁赢

if (candidateFreq > victimFreq) {

return true;

//如果相等,candidate小于5都当输了

} 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;

}

//如果相等且candidate大于5,则随机淘汰一个

int random = ThreadLocalRandom.current().nextInt();

return ((random & 127) == 0);

}

climb方法主要是用来调整 window size 的,使得 Caffeine 可以适应你的应用类型(如 OLAP 或 OLTP)表现出最佳的命中率。

下图是官方测试的数据:

我们看看 window size 的调整是怎么实现的。

调整时用到的默认比例数据:

//与上次命中率之差的阈值

static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;

//步长(调整)的大小(跟最大值maximum的比例)

static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;

//步长的衰减比例

static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;

/** Adapts the eviction policy to towards the optimal recency / frequency configuration. */

//climb方法的主要作用就是动态调整window区的大小(相应的,main区的大小也会发生变化,两个之和为100%)。

//因为区域的大小发生了变化,那么区域内的数据也可能需要发生相应的移动。

@GuardedBy(“evictionLock”)

void climb() {

if (!evicts()) {

return;

}

//确定window需要调整的大小

determineAdjustment();

//如果protected区有溢出,把溢出部分移动到probation区。因为下面的操作有可能需要调整到protected区。

demoteFromMainProtected();

long amount = adjustment();

if (amount == 0) {

return;

} else if (amount > 0) {

//增加window的大小

increaseWindow();

} else {

//减少window的大小

decreaseWindow();

}

}

下面分别展开每个方法来解释:

/** Calculates the amount to adapt the window by and sets {@link #adjustment()} accordingly. */

@GuardedBy(“evictionLock”)

void determineAdjustment() {

//如果frequencySketch还没初始化,则返回

if (frequencySketch().isNotInitialized()) {

setPreviousSampleHitRate(0.0);

setMissesInSample(0);

setHitsInSample(0);

return;

}

//总请求量 = 命中 + miss

int requestCount = hitsInSample() + missesInSample();

//没达到sampleSize则返回

//默认下sampleSize = 10 * maximum。用sampleSize来判断缓存是否足够”热“。

if (requestCount < frequencySketch().sampleSize) {

return;

}

//命中率的公式 = 命中 / 总请求

double hitRate = (double) hitsInSample() / requestCount;

//命中率的差值

double hitRateChange = hitRate - previousSampleHitRate();

//本次调整的大小,是由命中率的差值和上次的stepSize决定的

double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();

//下次的调整大小:如果命中率的之差大于0.05,则重置为0.065 * maximum,否则按照0.98来进行衰减

double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)

? HILL_CLIMBER_STEP_PERCENT * maximum() * (amount >= 0 ? 1 : -1)

: HILL_CLIMBER_STEP_DECAY_RATE * amount;

setPreviousSampleHitRate(hitRate);

setAdjustment((long) amount);

setStepSize(nextStepSize);

setMissesInSample(0);

setHitsInSample(0);

}

/** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */

//这个方法比较简单,减少protected区溢出的部分

@GuardedBy(“evictionLock”)

void demoteFromMainProtected() {

long mainProtectedMaximum = mainProtectedMaximum();

long mainProtectedWeightedSize = mainProtectedWeightedSize();

if (mainProtectedWeightedSize <= mainProtectedMaximum) {

return;

}

for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {

if (mainProtectedWeightedSize <= mainProtectedMaximum) {

break;

}

Node<K, V> demoted = accessOrderProtectedDeque().poll();

if (demoted == null) {

break;

}

demoted.makeMainProbation();

accessOrderProbationDeque().add(demoted);

mainProtectedWeightedSize -= demoted.getPolicyWeight();

}

setMainProtectedWeightedSize(mainProtectedWeightedSize);

}

/**

* Increases the size of the admission window by shrinking the portion allocated to the main

* space. As the main space is partitioned into probation and protected regions (80% / 20%), for

* simplicity only the protected is reduced. If the regions exceed their maximums, this may cause

* protected items to be demoted to the probation region and probation items to be demoted to the

* admission window.

*/

//增加window区的大小,这个方法比较简单,思路就像我上面说的

@GuardedBy(“evictionLock”)

void increaseWindow() {

if (mainProtectedMaximum() == 0) {

return;

}

long quota = Math.min(adjustment(), mainProtectedMaximum());

setMainProtectedMaximum(mainProtectedMaximum() - quota);

setWindowMaximum(windowMaximum() + quota);

demoteFromMainProtected();

for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {

Node<K, V> candidate = accessOrderProbationDeque().peek();

boolean probation = true;

if ((candidate == null) || (quota < candidate.getPolicyWeight())) {

candidate = accessOrderProtectedDeque().peek();

probation = false;

}

if (candidate == null) {

break;

}

int weight = candidate.getPolicyWeight();

if (quota < weight) {

break;

}

quota -= weight;

if (probation) {

accessOrderProbationDeque().remove(candidate);

} else {

setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);

accessOrderProtectedDeque().remove(candidate);

}

setWindowWeightedSize(windowWeightedSize() + weight);

accessOrderWindowDeque().add(candidate);

candidate.makeWindow();

}

setMainProtectedMaximum(mainProtectedMaximum() + quota);

setWindowMaximum(windowMaximum() - quota);

setAdjustment(quota);

}

/** Decreases the size of the admission window and increases the main’s protected region. */

//同上increaseWindow差不多,反操作

@GuardedBy(“evictionLock”)

void decreaseWindow() {

if (windowMaximum() <= 1) {

return;

}

long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));

setMainProtectedMaximum(mainProtectedMaximum() + quota);

setWindowMaximum(windowMaximum() - quota);

for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {

Node<K, V> candidate = accessOrderWindowDeque().peek();

if (candidate == null) {

break;

}

int weight = candidate.getPolicyWeight();

if (quota < weight) {

break;

}

quota -= weight;

setMainProtectedWeightedSize(mainProtectedWeightedSize() + weight);

setWindowWeightedSize(windowWeightedSize() - weight);

accessOrderWindowDeque().remove(candidate);

accessOrderProbationDeque().add(candidate);

candidate.makeMainProbation();

}

setMainProtectedMaximum(mainProtectedMaximum() - quota);

setWindowMaximum(windowMaximum() + quota);

setAdjustment(-quota);

}

以上,是 Caffeine 的 W-TinyLFU 策略的设计原理及代码实现解析。

异步的高性能读写

一般的缓存每次对数据处理完之后(读的话,已经存在则直接返回,不存在则 load 数据,保存,再返回;写的话,则直接插入或更新),但是因为要维护一些淘汰策略,则需要一些额外的操作,诸如:

  • 计算和比较数据的是否过期

  • 统计频率(像 LFU 或其变种)

  • 维护 read queue 和 write queue

  • 淘汰符合条件的数据

  • 等等。。。

这种数据的读写伴随着缓存状态的变更,Guava Cache 的做法是把这些操作和读写操作放在一起,在一个同步加锁的操作中完成,虽然 Guava Cache 巧妙地利用了 JDK 的 ConcurrentHashMap(分段锁或者无锁 CAS)来降低锁的密度,达到提高并发度的目的。但是,对于一些热点数据,这种做法还是避免不了频繁的锁竞争。Caffeine 借鉴了数据库系统的 WAL(Write-Ahead Logging)思想,即先写日志再执行操作,这种思想同样适合缓存的,执行读写操作时,先把操作记录在缓冲区,然后在合适的时机异步、批量地执行缓冲区中的内容。但在执行缓冲区的内容时,也是需要在缓冲区加上同步锁的,不然存在并发问题,只不过这样就可以把对锁的竞争从缓存数据转移到对缓冲区上。

ReadBuffer

在 Caffeine 的内部实现中,为了很好的支持不同的 Features(如 Eviction,Removal,Refresh,Statistics,Cleanup,Policy 等等),扩展了很多子类,它们共同的父类是BoundedLocalCache,而readBuffer就是作为它们共有的属性,即都是用一样的 readBuffer,看定义:

final Buffer<Node<K, V>> readBuffer;

readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()

? new BoundedBuffer<>()

: Buffer.disabled();

上面提到 Caffeine 对每次缓存的读操作都会触发afterRead

/**

* Performs the post-processing work required after a read.

* @param node the entry in the page replacement policy

* @param now the current time, in nanoseconds

* @param recordHit if the hit count should be incremented

*/

void afterRead(Node<K, V> node, long now, boolean recordHit) {

if (recordHit) {

statsCounter().recordHits(1);

}

//把记录加入到readBuffer

//判断是否需要立即处理readBuffer

//注意这里无论offer是否成功都可以走下去的,即允许写入readBuffer丢失,因为这个

boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);

if (shouldDrainBuffers(delayable)) {

scheduleDrainBuffers();

}

refreshIfNeeded(node, now);

}

/**

* Returns whether maintenance work is needed.

* @param delayable if draining the read buffer can be delayed

*/

//caffeine用了一组状态来定义和管理“维护”的过程

boolean shouldDrainBuffers(boolean delayable) {

switch (drainStatus()) {

case IDLE:

return !delayable;

case REQUIRED:

return true;

case PROCESSING_TO_IDLE:

case PROCESSING_TO_REQUIRED:

return false;

default:

throw new IllegalStateException();

}

}

重点看BoundedBuffer

/**

* A striped, non-blocking, bounded buffer.

* @author ben.manes@gmail.com (Ben Manes)

* @param  the type of elements maintained by this buffer

*/

final class BoundedBuffer extends StripedBuffer

它是一个 striped、非阻塞、有界限的 buffer,继承于StripedBuffer类。下面看看StripedBuffer的实现:

/**

* A base class providing the mechanics for supporting dynamic striping of bounded buffers. This

* implementation is an adaption of the numeric 64-bit {@link java.util.concurrent.atomic.Striped64}

* class, which is used by atomic counters. The approach was modified to lazily grow an array of

* buffers in order to minimize memory usage for caches that are not heavily contended on.

* @author dl@cs.oswego.edu (Doug Lea)

* @author ben.manes@gmail.com (Ben Manes)

*/

abstract class StripedBuffer implements Buffer

这个StripedBuffer设计的思想是跟Striped64类似的,通过扩展结构把竞争热点分离。

具体实现是这样的,StripedBuffer维护一个Buffer[]数组,每个元素就是一个RingBuffer,每个线程用自己threadLocalRandomProbe属性作为 hash 值,这样就相当于每个线程都有自己“专属”的RingBuffer,就不会产生竞争啦,而不是用 key 的hashCode作为 hash 值,因为会产生热点数据问题。

看看StripedBuffer的属性

/** Table of buffers. When non-null, size is a power of 2. */

//RingBuffer数组

transient volatile Buffer @Nullable[] table;

//当进行resize时,需要整个table锁住。tableBusy作为CAS的标记。

static final long TABLE_BUSY = UnsafeAccess.objectFieldOffset(StripedBuffer.class, “tableBusy”);

static final long PROBE = UnsafeAccess.objectFieldOffset(Thread.class, “threadLocalRandomProbe”);

/** Number of CPUS. */

static final int NCPU = Runtime.getRuntime().availableProcessors();

/** The bound on the table size. */

//table最大size

static final int MAXIMUM_TABLE_SIZE = 4 * ceilingNextPowerOfTwo(NCPU);

/** The maximum number of attempts when trying to expand the table. */

//如果发生竞争时(CAS失败)的尝试次数

static final int ATTEMPTS = 3;

/** Table of buffers. When non-null, size is a power of 2. */

//核心数据结构

transient volatile Buffer @Nullable[] table;

/** Spinlock (locked via CAS) used when resizing and/or creating Buffers. */

transient volatile int tableBusy;

/** CASes the tableBusy field from 0 to 1 to acquire lock. */

final boolean casTableBusy() {

return UnsafeAccess.UNSAFE.compareAndSwapInt(this, TABLE_BUSY, 0, 1);

}

/**

* Returns the probe value for the current thread. Duplicated from ThreadLocalRandom because of

* packaging restrictions.

*/

static final int getProbe() {

return UnsafeAccess.UNSAFE.getInt(Thread.currentThread(), PROBE);

}

offer方法,当没初始化或存在竞争时,则扩容为 2 倍。

实际是调用RingBuffer的 offer 方法,把数据追加到RingBuffer后面。

@Override

public int offer(E e) {

int mask;

int result = 0;

Buffer buffer;

//是否不存在竞争

boolean uncontended = true;

Buffer[] buffers = table

//是否已经初始化

if ((buffers == null)

|| (mask = buffers.length - 1) < 0

//用thread的随机值作为hash值,得到对应位置的RingBuffer

|| (buffer = buffers[getProbe() & mask]) == null

//检查追加到RingBuffer是否成功

|| !(uncontended = ((result = buffer.offer(e)) != Buffer.FAILED))) {

//其中一个符合条件则进行扩容

expandOrRetry(e, uncontended);

}

return result;

}

/**

* Handles cases of updates involving initialization, resizing, creating new Buffers, and/or

* contention. See above for explanation. This method suffers the usual non-modularity problems of

* optimistic retry code, relying on rechecked sets of reads.

* @param e the element to add

* @param wasUncontended false if CAS failed before call

*/

//这个方法比较长,但思路还是相对清晰的。

@SuppressWarnings(“PMD.ConfusingTernary”)

final void expandOrRetry(E e, boolean wasUncontended) {

int h;

if ((h = getProbe()) == 0) {

ThreadLocalRandom.current(); // force initialization

h = getProbe();

wasUncontended = true;

}

boolean collide = false; // True if last slot nonempty

for (int attempt = 0; attempt < ATTEMPTS; attempt++) {

Buffer[] buffers;

Buffer buffer;

int n;

if (((buffers = table) != null) && ((n = buffers.length) > 0)) {

if ((buffer = buffers[(n - 1) & h]) == null) {

if ((tableBusy == 0) && casTableBusy()) { // Try to attach new Buffer

boolean created = false;

try { // Recheck under lock

Buffer[] rs;

int mask, j;

if (((rs = table) != null) && ((mask = rs.length) > 0)

&& (rs[j = (mask - 1) & h] == null)) {

rs[j] = create(e);

created = true;

}

} finally {

tableBusy = 0;

}

if (created) {

break;

}

continue; // Slot is now non-empty

}

collide = false;

} else if (!wasUncontended) { // CAS already known to fail

wasUncontended = true;      // Continue after rehash

} else if (buffer.offer(e) != Buffer.FAILED) {

break;

} else if (n >= MAXIMUM_TABLE_SIZE || table != buffers) {

collide = false; // At max size or stale

} else if (!collide) {

collide = true;

} else if (tableBusy == 0 && casTableBusy()) {

try {

if (table == buffers) { // Expand table unless stale

table = Arrays.copyOf(buffers, n << 1);

}

} finally {

tableBusy = 0;

}

collide = false;

continue; // Retry with expanded table

}

h = advanceProbe(h);

} else if ((tableBusy == 0) && (table == buffers) && casTableBusy()) {

boolean init = false;

try { // Initialize table

if (table == buffers) {

@SuppressWarnings({“unchecked”, “rawtypes”})

Buffer[] rs = new Buffer[1];

rs[0] = create(e);

table = rs;

init = true;

}

} finally {

tableBusy = 0;

}

if (init) {

break;

}

}

}

}

最后看看RingBuffer,注意RingBufferBoundedBuffer的内部类。

/** The maximum number of elements per buffer. */

static final int BUFFER_SIZE = 16;

// Assume 4-byte references and 64-byte cache line (16 elements per line)

//256长度,但是是以16为单位,所以最多存放16个元素

static final int SPACED_SIZE = BUFFER_SIZE << 4;

static final int SPACED_MASK = SPACED_SIZE - 1;

static final int OFFSET = 16;

//RingBuffer数组

final AtomicReferenceArray buffer;

//插入方法

@Override

public int offer(E e) {

long head = readCounter;

long tail = relaxedWriteCounter();

//用head和tail来限制个数

long size = (tail - head);

if (size >= SPACED_SIZE) {

return Buffer.FULL;

}

//tail追加16

if (casWriteCounter(tail, tail + OFFSET)) {

//用tail“取余”得到下标

int index = (int) (tail & SPACED_MASK);

//用unsafe.putOrderedObject设值

buffer.lazySet(index, e);

return Buffer.SUCCESS;

}

//如果CAS失败则返回失败

return Buffer.FAILED;

}

//用consumer来处理buffer的数据

@Override

public void drainTo(Consumer consumer) {

long head = readCounter;

long tail = relaxedWriteCounter();

//判断数据多少

long size = (tail - head);

if (size == 0) {

return;

}

do {

int index = (int) (head & SPACED_MASK);

E e = buffer.get(index);

if (e == null) {

// not published yet

break;

}

buffer.lazySet(index, null);

consumer.accept(e);

//head也跟tail一样,每次递增16

head += OFFSET;

} while (head != tail);

lazySetReadCounter(head);

}

注意,ring buffer 的 size(固定是 16 个)是不变的,变的是 head 和 tail 而已。

总的来说ReadBuffer有如下特点:

  • 使用 Striped-RingBuffer来提升对 buffer 的读写

  • 用 thread 的 hash 来避开热点 key 的竞争

  • 允许写入的丢失

WriteBuffer

writeBufferreadBuffer不一样,主要体现在使用场景的不一样。本来缓存的一般场景是读多写少的,读的并发会更高,且 afterRead 显得没那么重要,允许延迟甚至丢失。写不一样,写afterWrite不允许丢失,且要求尽量马上执行。Caffeine 使用MPSC(Multiple Producer / Single Consumer)作为 buffer 数组,实现在MpscGrowableArrayQueue类,它是仿照JCToolsMpscGrowableArrayQueue来写的。

MPSC 允许无锁的高并发写入,但只允许一个消费者,同时也牺牲了部分操作。

MPSC 我打算另外分析,这里不展开了。

TimerWheel

除了支持expireAfterAccessexpireAfterWrite之外(Guava Cache 也支持这两个特性),Caffeine 还支持expireAfter。因为expireAfterAccessexpireAfterWrite都只能是固定的过期时间,这可能满足不了某些场景,譬如记录的过期时间是需要根据某些条件而不一样的,这就需要用户自定义过期时间。

先看看expireAfter的用法

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

文末

我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档

  • 第一个要分享给大家的就是算法和数据结构

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 第二个就是数据库的高频知识点与性能优化

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 第三个则是并发编程(72个知识点学习)

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

  • 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料

网易严选Java开发三面面经:HashMap+JVM+索引+消息队列

还有更多的Redis、MySQL、JVM、Kafka、微服务、Spring全家桶等学习笔记这里就不一一列举出来

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
cer / Single Consumer)作为 buffer 数组,实现在MpscGrowableArrayQueue类,它是仿照JCToolsMpscGrowableArrayQueue来写的。

MPSC 允许无锁的高并发写入,但只允许一个消费者,同时也牺牲了部分操作。

MPSC 我打算另外分析,这里不展开了。

TimerWheel

除了支持expireAfterAccessexpireAfterWrite之外(Guava Cache 也支持这两个特性),Caffeine 还支持expireAfter。因为expireAfterAccessexpireAfterWrite都只能是固定的过期时间,这可能满足不了某些场景,譬如记录的过期时间是需要根据某些条件而不一样的,这就需要用户自定义过期时间。

先看看expireAfter的用法

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-e432DJzm-1712267980505)]

[外链图片转存中…(img-Xtg3nR3F-1712267980505)]

[外链图片转存中…(img-HHhpgtgk-1712267980506)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

文末

我将这三次阿里面试的题目全部分专题整理出来,并附带上详细的答案解析,生成了一份PDF文档

  • 第一个要分享给大家的就是算法和数据结构

[外链图片转存中…(img-n2FOKVmX-1712267980506)]

  • 第二个就是数据库的高频知识点与性能优化

[外链图片转存中…(img-lcsHIIAU-1712267980506)]

  • 第三个则是并发编程(72个知识点学习)

[外链图片转存中…(img-SdTCsMDV-1712267980507)]

  • 最后一个是各大JAVA架构专题的面试点+解析+我的一些学习的书籍资料

[外链图片转存中…(img-BMLZRDDt-1712267980507)]

还有更多的Redis、MySQL、JVM、Kafka、微服务、Spring全家桶等学习笔记这里就不一一列举出来

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值