2021-07-23日 晚9点
本文主要意思是:学习LoadingCache本地缓存特性和源码。
文中会引用大量的官网观点.
一定要看get源码,注意:回收/删除的动作,可能并不是自动删除,因为涉及到工作线程和用户线程竞争锁。
案例
private static final LoadingCache<String, Long> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.SECONDS)
.removalListener((RemovalListener<String, Long>) notification -> {
System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
})
.build(new CacheLoader<String, Long>() {
@Override
public Long load(String key) throws Exception {
long curTime = System.currentTimeMillis();
System.out.println("curTime=" + curTime);
return curTime;
}
});
适用性
官方说明
Guava Cache 与ConcurrentMap 很相似,但是也不完全相同。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显示的删除。
相对地,Guava Cache 为了限制内存占用,通常都会设定自定回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。
后面这句话其实就是说LoadingCache自动加载缓存是优点: get(key, load); || 重写 load 自动触发 --> 这里的自动其实都不是自动的, 必须要有读/写请求来的时候, 才会触发
通常来说,Guava Cache适用于:
- 你愿意消耗一些机器内存空间来提升速度。
- 你预料到某些键会被查询一次以上。
- 缓存中存放的数据总量不会超出内存总量。(因为Guava Cache是单个应用运行时的本地缓存,它不把数据存储到文件或外部服务器。如果此时不符合你的业务需求,请尝试Memcached这类工具。)
如果以上场景符合你的业务需要, Guava Cache就适合你。
注意: 如果你不需要Cache 中的特性, 使用ConcurrentHashMap 有更好的内存效率–但 Cache的大多数特性都很难基于旧的ConcurrentMap 复制, 甚至根本不可能做到.
也就是说,官方建议我们使用Guava Cache (在符合场景的前提下)
功能
LoadingCache 是一个支持多线程并发读写、高性能、通用的in-heap(堆)本地缓存,有以下功能:
- 核心功能: 高性能线程安全的in-heap Map 数据结构(类似于ConcurrentHashMap)
- 支持key不存在时按照给定的CacheLoader 的loader方法进行loading。如果有多个线程同时get一个不存在的key,那么会有一个线程负责load,其他线程阻塞wait等待。
get方法中:
// at this point e is either null or expired;
// 翻译: 此时get获取到的元素为 null / 过期
return lockedGetOrLoad(key, hash, loader);
// 啥意思? 加锁去get或者执行load方法。
lockedGetOrLoad 方法
try {
// Synchronizes on the entry to allow failing fast when a recursive load is
// detected. This may be circumvented when an entry is copied, but will fail fast most
// of the time.
synchronized (e) {
return loadSync(key, hash, loadingValueReference, loader);
}
} finally {
statsCounter.recordMisses(1);
}
- 支持entry的evitBySize, 这是一个LRU cache的基本功能. --> 啥意思
- 支持对entry设置过期时间, 按照最后访问/写入的时间来淘汰entry.
- 支持传入entry删除事件监听器, 当entry被删除或者淘汰时执行监听器逻辑.
- 支持对entry进行定时reload, 默认使用loader逻辑进行同步reload (建议重写 CacheLoader的reload方法实现异步reload)
原生CacheLoader 此时执行这个方法是'同步'的, 不过返回值确是ListenableFuture
@Override
public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception{
// return Futures.immediateFuture(load(key));
return super.reload(key, oldValue);
// 这里直接调用了load方法去获取返回值,然后将返回值设置到一个ImmediateFuture类型的future中。本质上没有异步执行。
}
如果需要refresh是异步的, 不影响用户线程.
此时需要: 自己使用写一个类AsyncReload, 实现reload方法. 实际上就是启动一个'异步线程',不阻塞用户线程.
相对于原生的CacheLoader, 也只是只少了'一个'线程阻塞.
AsyncReload
final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
@Override
public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
ListenableFutureTask<V> task = create(() -> load(key));
service.execute(task);
return task;
}
.build(new AsyncReload<String, Long>() {
load..
@Override
public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
return super.reload(key, oldValue);
}
});
- 支持WeakReference封装key, 当key在应用程序里没有别的引用是, jvm会gc回收key对象, LoadingCache也会得到通知从而进行清理.
- 支持用WeakReference 或者 SoftReference 封装value对象
- 支持缓存相关运行数据的统计.
关于load方法和reload方法说明:
load方法: 第一次加载, 或key对应的value不存在(已经过期)会触发load方法.
load方法是同步的, 对于同一个key, 多次请求只能触发一次加载. 比如thread1访问key1,
发现cache中不存在key1, 触发key1的load, 同时, 在加载过程完成之前, 其他线程都访问key1,
此时这些访问都不会触发key1的load加载, 因为加载load方法被synchronized修饰.
synchronized (e) {
return loadSync(key, hash, loadingValueReference, loader);
}
reload方法:
1. 当cache中有值, 但是需要刷新的该值的时候就会触发reload方法. ('不是自动的')
2. loadingCache的所有更新操作都是'依赖读写操作'触发的, 因为内部没有定时任务或者时钟信号.
例如, 上一次写入之后超过了refresh设置的更新时间, 但是之后没有cache的访问了,
此时之后下一次get的时候才会进行触发refresh.....
比如: 20s 后过期, 5s刷新一次, main开始, 写入一次. 此时初始的5s内(包括第5s)
没有get请求, 此时不会自动触发refresh, 而是后面15s内来了一个get请求,
此时才会触发refresh.
3. 对于同一个key, '多次请求只有一个线程触发reload, 其他请求直接返回旧值'.
比如: 7s的时候有两个线程访问cache, 只会有一个线程refresh.
如果是原生CacheLoader, 此时这两个线程是同步执行的. 其中有一个必然会触发refresh, 就看操作系统把锁给哪个线程了. 'A先refresh, B返回旧值'.
如果是AsyncLoader重写reload, create一个线程异步执行, '这两个线程返回旧值'. '异步线程执行触发refresh'.
只配置expireAfterWrite: load和refresh都阻塞.
在配置refreshAfterWrite之后, 执行reload时:
原生: 一个用户线程去refresh, 其他线程返回旧值.
异步: 所有用户线程返回旧值, 新创建一个异步线程去refresh, '所以比原生只少了一个线程的阻塞'
主要的类和接口
- CacheBuilder
- Builder设计模式, 更友好的支持多参数的类的构建. CacheBuilder在build方法中, 会前面设置的参数, 全部传递个LoaclCache, 它自己实际不参与任何计算. 不错
- CacheLoader
- 抽象类, 用户从数据源加载数据, 业务也可以继承它, 重写load, reload.
- Cache
- 接口, 定义get, put, invalidate等方法, 这里只有cache的增删改操作, 没有数据加载的操作.
- AbstractCache
- 抽象类, 继承自Cache接口. 其中批量操作都是循环执行单次行为, 而单次行为都没有具体定义.
- LoadingCache
- 接口, 继承自Cache接口. 定义get, getUnchecked, getAll等操作, 这些操作都会从数据源load数据.
- AbstractLoadingCache
- 抽象类, 继承自AbstractCache, 实现LoadingCache接口.
- LoadCache(核心)
- guava cache核心类, 包含了guava cache的数据结构以及基本的缓存的操作办法.
- LocalManualCache
- LocalCache内部静态类, 实现Cache接口. 其内部的增删改缓存操作全部调用成员变量 loaclCache的相关方法. (缓存的手动加载: 手动get)
- LocalLoadingCache
- LocalCache内部静态类, 继承自LocalManualCache类, 实现LoadingCache接口. 其所有操作也是调用成员变量localCache的相关方法. (缓存的自动加载: 自动get)
接口和类关系图: LocalLoadingCache, LocalManualCache, 是loaclCache内部静态类
使用
常用参数
- expireAfterWrite: 当创建或写之后的固定有效期到达时, 数据会过期, 但是不会删除, 因为下一次请求来的时候, 会获取有效值, 无效则删除, 后同步指定load方法.
V value = getLiveValue(e, now); // 获取存活的值 (如果expired, 则expired 包含: recencyQueue -> 入AccessQueue,删除(writeQueue删除, AccessQueue队列删除))
V getLiveValue(ReferenceEntry<K, V> entry, long now) {
if (entry.getKey() == null) {
tryDrainReferenceQueues();
return null;
}
V value = entry.getValueReference().get();
if (value == null) {
tryDrainReferenceQueues();
return null;
}
if (map.isExpired(entry, now)) { // 如果需要过期
tryExpireEntries(now);
return null;
}
return value;
}
void tryExpireEntries(long now) {
if (tryLock()) {
try {
expireEntries(now);
} finally {
unlock();
}
}
void expireEntries(long now) {
drainRecencyQueue(); // 最近读队列recencyQueue -> 入AccessQueue(最近访问队列)
ReferenceEntry<K, V> e;
while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
}
- expireAfterAccess: 当创建或者或读之后的固定有效期到达时, 同上. 读写操作都会重置访问时间, 但asMap方法不会.
- refreshAfterWrite: 当创建或者写之后的固定有效期到达时, 且新请求过来时, 数据会被自动刷新.(注意不是删除而是刷新(原生同步, 改造异步), 其他线程返回旧值)
- concurrencyLevel: 更新操作之间允许的并发, 也就是segment的数量.
- maximumSize: 指定缓存可以包含的最大entries数.
- maximumWeight: 指定缓存可能包含的entries数的最大权重, 使用此方法需要先调用weigher方法.
- weigher: 指定确定entries的weight的wegher, 通过重写weigh(K key, V value)方法, 来确定每一个Entries的weight.
- weakKeys: 指定将缓存中存储的每个键都包装在WeakReference(默认情况下, 使用强引用)
- weakValues: 指定将缓存中存储的每个值都包装在WeakReference(默认情况下, 使用强引用)
- softValues: 指定将缓存中存储的每个值都包装在SoftReference(默认情况下, 使用强引用)
segment数据结构
WriteQueue:按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。
AccessQueue:按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。
加载
获取
- get 如果没有, cacheLoader 向缓存原子地加载新值.
- getAll<Iterable<? extend K>>: 批量查询. 默认情况下, 对每个不在缓存中的键, getAll方法会单独调用CacheLoader.load来加载缓存项. 如果批量的加载比多个单独加载高效, 可以重载CacheLoader.loadAll 来利用这一点. getAll(iterable)的性能也会提升.
注: CacheLoader.loadAll 的实现可以为没有明确请求的键加载缓存值. 例如, 为某组中的任意键计算值时, 能够获取该组中的所有键值, loadAll方法就可以实现为在同一时间获取该组的其他键值. getAll(Iterable<? extends K>)方法会调用loadAll, 但会筛选结果, 只会返回请求的键值对.
显示插入
使用cache.put(key, value)方法可以直接向缓存中插入值, 这会直接覆盖掉给定键之前映射的值. 使用Cache.asMap()视图提供的任何方法也能修改缓存. 但一定要注意, asMap视图的任何方法都不能保证缓存项被原子地加载到缓存.
进一步说. asMap视图的原子运算在Guava Cache的原子加载范畴之外, 所以相比于Cache.asMap().putIfAbsent(K, V), Cache.get(K, Callable)应该总是优先使用.
源码分析 不外乎 get/put/remove 因为它类似于concurrentMap
LocalCache构建
private static final LoadingCache<String, Long> notifyNoticeAndEmailCache = CacheBuilder.newBuilder() //
.expireAfterWrite(
20, TimeUnit.SECONDS) // 缓存项在给定时间内没有被写访问(创建或覆盖),则回收: 10s失效
.maximumSize(10)
.removalListener((RemovalListener<String, Long>) notification -> {
System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
})
.refreshAfterWrite(5, TimeUnit.SECONDS)
.build(new AsyncReLoad<String, Long>() {
@Override
public Long load(String key) {
long curTime = System.currentTimeMillis();
System.out.println("curTime=" + curTime);
return curTime;
}
@Override
public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
System.out.println("异步线程, 执行");
return super.reload(key, oldValue);
}
});
以上是一个构建LoadingCache的小李子. 流程如下:
- 采用建造者模式, 首先创造出CacheBuilder.
- 使用流式风格, 依次设置参数.
- build方法创建完成.
其中以上参数都是CacheBuilder的属性, 在使用Build方法时, CacheBuilder被当做参数传入LoaclLoadingCache或LoaclManualCache的构造器中进行构造.
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<>(this, loader); // 多个了一个loader
}
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
checkWeightWithWeigher();
checkNonLoadingCache();
return new LocalCache.LocalManualCache<>(this);
}
从上面代码可以看出来, 区别是在创建时是否传入CacheLoader, 分为’自动加载’与手动加载. 同时创建缓存时, 也将CacheBuilder传入(this).
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
implements LoadingCache<K, V> {
LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
super(new LocalCache<K, V>(builder, checkNotNull(loader)));
}
static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {
final LocalCache<K, V> localCache;
LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
this(new LocalCache<K, V>(builder, null));
}
private LocalManualCache(LocalCache<K, V> localCache) {
this.localCache = localCache;
}
从上面代码看出, LocalLoadingCache继承自LoadManualCache, 区别是: CacherLoader是否自主传入进来. 有/没有.
两个类都有一个LocalCache的引用, 其内部的增删改缓存操作都是调用成员变量localCache. 但是它两都是LocalCache的静态内部类.
LocalCache初始化
LocalCache首先把builder中的所有属性都取出来, 然后赋值给自己的所有成员变量.
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>
static final int MAXIMUM_CAPACITY = 1 << 30; // 缓存最大容量, 该数值必须为2的幂, 同时小于这个最大值 2^30
static final int MAX_SEGMENTS = 1 << 16; // segment数组最大容量
static final int CONTAINS_VALUE_RETRIES = 3; // containsValue方法的重试次数
static final int DRAIN_THRESHOLD = 0x3F; // 在更新缓存的最近排序信息之前, 每个段可以缓冲的缓存访问操作的数量. 这是为了避免锁争用, 通过记录读的记忆和延迟获取锁,直到超过阈值或发生突变.
static final int DRAIN_MAX = 16; // 一次清理操作中, 最大移除的entry数量
final int segmentMask; // 定位segment
final int segmentShift; // 定位segment, 同时让entry分布均匀,尽量平均分布在每个segment[i]中
final Segment<K, V>[] segments;
final int concurrencyLevel; // 更新操作之间允许的并发, 也就是**segment**的数量.
下来我们来看一下LocalCache的构造
LocalCache(
CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
// 并发程度, 根据传入参数和默认最大值中取小值
// 如果没有指定参数的情况下, concurrencyLevel == UNSET_INT == -1
// 然后getConcurrencyLevel 方法判断上面这个方式成立, 返回默认值4
// 否则返回用户传递进来的参数
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
// 键值的引用类型, 没有指定的话, 默认为强引用类型.
keyStrength = builder.getKeyStrength();
valueStrength = builder.getValueStrength();
// 判断相同的方法, 强引用类型就是Equivalence.equals() 各个引用类型的判断下相同的方法都不同, 在 enum Strength 中可以看到
keyEquivalence = builder.getKeyEquivalence();
valueEquivalence = builder.getValueEquivalence();
// 最大权重
maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos();
// 移除消息监听器
removalListener = builder.getRemovalListener();
// 如果指定了移除消息监听器的话, 会创建一个队列, 临时保存移除的内容
removalNotificationQueue =
(removalListener == NullListener.INSTANCE)
? LocalCache.<RemovalNotification<K, V>>discardingQueue()
: new ConcurrentLinkedQueue<RemovalNotification<K, V>>();
ticker = builder.getTicker(recordsTime());
// 创建新的缓存内容(entry) 的工厂, 会根据引用类型选择对应的工厂
entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
globalStatsCounter = builder.getStatsCounterSupplier().get();
defaultLoader = loader;
// 初始化缓存容量, 默认为16
int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = (int) Math.min(initialCapacity, maxWeight);
}
// Find the lowest power-of-two segmentCount that exceeds concurrencyLevel, unless
// maximumSize/Weight is specified in which case ensure that each segment gets at least 10
// entries. The special casing for size-based eviction is only necessary because that eviction
// happens per segment instead of globally, so too many segments compared to the maximum size
// will result in random eviction behavior.
int segmentShift = 0;
int segmentCount = 1;
// 根据并发程度来计算segement的个数, (大于等于concurrencyLevel的最小的2次幂, 这里即为 4)
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
++segmentShift;
segmentCount <<= 1;
}
// 这里的segmentShift和segmentMask用来打散entry, 让缓存内容尽量均匀分布在每个segment下
this.segmentShift = 32 - segmentShift;
segmentMask = segmentCount - 1;
// 初始化segment数组, 大小为4
this.segments = newSegmentArray(segmentCount);
// 每个segment的容量, 总容量 / 数组个数, 向上取整, 16 / 4 = 4
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
}
// segmentSize 为小于segmentCapacity的最大的2的次幂, 这里为4
int segmentSize = 1;
while (segmentSize < segmentCapacity) {
segmentSize <<= 1;
}
// 初始化每个segment[i]
// 注意: 判断是否有权重, 根据权重来初始化
if (evictsBySize()) {
// Ensure sum of segment max weights = overall max weights
long maxSegmentWeight = maxWeight / segmentCount + 1;
long remainder = maxWeight % segmentCount;
for (int i = 0; i < this.segments.length; ++i) {
if (i == remainder) {
maxSegmentWeight--;
}
this.segments[i] =
createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
}
} else {
// 权重为-1, 也就是基本上没有权重
for (int i = 0; i < this.segments.length; ++i) {
this.segments[i] =
createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
}
}
}
2021年7月25日早11点:
Segment初始化
static class Segment<K, V> extends ReentrantLock
@Weak final LocalCache<K, V> map;
volatile @MonotonicNonNull AtomicReferenceArray<ReferenceEntry<K, V>> table;
final @Nullable ReferenceQueue<K> keyReferenceQueue;
final @Nullable ReferenceQueue<V> valueReferenceQueue;
final Queue<ReferenceEntry<K, V>> recencyQueue;
final AtomicInteger readCount = new AtomicInteger();
@GuardedBy("this")
final Queue<ReferenceEntry<K, V>> writeQueue;
@GuardedBy("this")
final Queue<ReferenceEntry<K, V>> accessQueue;
初始化:
Segment(
LocalCache<K, V> map,
int initialCapacity,
long maxSegmentWeight,
StatsCounter statsCounter) {
this.map = map;
this.maxSegmentWeight = maxSegmentWeight;
this.statsCounter = checkNotNull(statsCounter);
initTable(newEntryArray(initialCapacity));
keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null;
valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null;
recencyQueue =
map.usesAccessQueue()
? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
writeQueue =
map.usesWriteQueue()
? new WriteQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
accessQueue =
map.usesAccessQueue()
? new AccessQueue<K, V>()
: LocalCache.<ReferenceEntry<K, V>>discardingQueue();
}
从segment的数据结构来看, 它有以下几个队列和table, map组成:
- keyReferenceQueue
- valueReferenceQueue
keyReference和valueReferenceQueue 在build时设置了soft或weak时会被初始化,
用于接收gc回收key/value对象的通知, 队列中的元素是引用key/value的reference.
显然referenceQueue是线程安全的, 队列的生成者是jvm的gc线程, 消费者是LoadingCache自身.
当key对象除了reference之外没有别的地方引用时, 下次gc时对象会被回收,
同时referenceQueue会受到通知, 然后对应的entry会被清理掉.
其实就是一句话, 在弱引用/软引用的情况下gc回收的内容会放入这两个队列. 加速gc回收.
- recencyQueue
recencyQueue 启用条件和accessQueue一样。
- 每次’访问操作’(读)都会将该entry加入到队列尾部,并更新accessTime。
- 如果遇到写入操作,则将该队列内容排干,如果accessQueue队列中持有该这些 entry,然后将这些entry add到accessQueue队列。
- 注意,因为accessQueue是非线程安全的,所以如果每次访问entry时就将该entry加入到accessQueue队列中,就会导致并发问题。
- 所以这里每次访问先将entry临时加入到并发安全的ConcurrentLinkedQueue队列中,也就是recencyQueue中。
- 在写入的时候通过加锁的方式,将recencyQueue中的数据添加到accessQueue队列中。
- 如此看来,recencyQueue是为accessQueue服务的.
- accessQueue
按照访问时间进行排序的元素队列, 访问(包括写入)一个元素时会把它加入到队列尾部.
…
- writeQueue
按照写入时间进行排序的元素队列, 写入一个元素时会把它加入到队列尾部.
…
get
下面我们来介绍一下 get源码, !!! —重要
@Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
一路进来: 其实就定位到了那个segment中entry中的值
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
return segmentFor(hash).get(key, hash, loader);
}
Segment#get()
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
// count保存的是该segment中entry的数量, 如果为0, 就直接去加载
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
// 获取entry, 并做一些清理工作, 在操作开始/结束. 默认清理DRAIN_MAX = 16次.
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
// 在entry无效, 过期, 正在载入都会返回null, 如果返回不为null, 即为正常命中
V value = getLiveValue(e, now);
if (value != null) {
// 如果有expiredAfterAccess, 更新entry的accessTime. finaly, 丢入recencyQueue 最近访问队列, 为啥不进入accessQueue. TODO 后面会解释...
recordRead(e, now);
// 性能统计
statsCounter.recordHits(1);
// 根据用户是否设置刷新时间&距离上次访问/写入时间-短时间过期, 进行刷新或者直接返回.
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
// 如果正在被加载, 等待加载完成.
return waitForLoadingValue(e, key, valueReference);
}
}
}
// 如果不存在或者过期, 就用loader方式加载, loader从上面来: 1. default, 2. get(key, loader)
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw ee;
} finally {
// 清理, 清除操作总是伴随着写入进行的, 如果是过长时间没有写入, 那我们就根据读线程来完成清理.
// 很长时间是多长?还记得前面有一个参数DRAIN_THRESHOLD = 0x3F = 3F = 63
// (readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0
// 也就是说 读64次就会清理一次, 具体是怎样清理的, 给你们透露一下, <font color='red'>**上面5个队列都会清理**</font>.......
postReadCleanup();
}
}
Segment#lockedGetOrLoad():
V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
ReferenceEntry<K, V> e;
ValueReference<K, V> valueReference = null;
LoadingValueReference<K, V> loadingValueReference = null;
boolean createNewEntry = true;
// 因为Segment继承自ReentrantLock, 所以有这个方法. 为啥会加锁, 因为有多线程.
lock();
try {
// re-read ticker once inside the lock
long now = map.ticker.read();
preWriteCleanup(now);
// 该segment下面的HashTable
int newCount = this.count - 1;
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
//这里也是为什么table的大小要为2的幂(最后index范围刚好在0-table.length()-1), 数据需要分散. 如果是奇数, 那么 & 之后永远在 奇数槽
int index = hash & (table.length() - 1);
ReferenceEntry<K, V> first = table.get(index);
//遍历链表
for (e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
// keyEquivalence 是指匹配规则, equivalent 类似equals
valueReference = e.getValueReference();
// 如果正在载入, 此时不需要创建, 等待即可
if (valueReference.isLoading()) {
createNewEntry = false;
} else {
V value = valueReference.get();
// 被gc回收 (配置了weakReference / SoftReference 弱引用/软引用)
if (value == null) {
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else if (map.isExpired(e, now)) {
// 过期了呀
// This is a duplicate check, as preWriteCleanup already purged expired
// entries, but let's accommodate an incorrect expiration queue.
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
} else {
// 记录被读,== 更新accessTime, 和recency队列, 正常返回value值
recordLockedRead(e, now);
statsCounter.recordHits(1);
// we were concurrent with loading; don't consider refresh
return value;
}
// 针对被gc回收和过期的情况, 从写队列和最近访问队列中移除,
// 因为后面重新载入后, 会再次添加到队列中
// immediately reuse invalid entries
writeQueue.remove(e);
accessQueue.remove(e);
this.count = newCount; // write-volatile
}
break;
}
}
// 注意: 下面这段代码只是说正在加载中, 实际上没有把具体的值加载进去, 只是为了阻塞其他线程.
if (createNewEntry) {
// 先创建一个LoadingValueReference, 表示正在载入中
loadingValueReference = new LoadingValueReference<>();
if (e == null) {
// 如果当前链表为null, 先创建一个头结点
e = newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
unlock();
// 执行清理, 确实是这样, 因为官网规定, 每次操作都会清空数据.
postWriteCleanup();
}
if (createNewEntry) {
try {
// Synchronizes on the entry to allow failing fast when a recursive load is
// detected. This may be circumvented when an entry is copied, but will fail fast most
// of the time.
synchronized (e) {
// 同步加载
return loadSync(key, hash, loadingValueReference, loader);
}
} finally {
// 记录没有被命中
statsCounter.recordMisses(1);
}
} else {
// The entry already exists. Wait for loading.
return waitForLoadingValue(e, key, valueReference);
}
}
Segment#loadSync():
V loadSync(
K key,
int hash,
LoadingValueReference<K, V> loadingValueReference,
CacheLoader<? super K, V> loader)
throws ExecutionException {
// 调用loader, 看着是异步, 要注意不是异步!!!!!
ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
return getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
}
Segment#getAndRecordStats():
// 等待载入, 并且记录是否成功
V getAndRecordStats(
K key,
int hash,
LoadingValueReference<K, V> loadingValueReference,
ListenableFuture<V> newValue)
throws ExecutionException {
V value = null;
try {
value = getUninterruptibly(newValue);
if (value == null) {
throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}
// 性能统计是否加载成功!!
statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos());
`真正去把数据,载入到缓存中 (当前还是loadingValueReference, 表示isLoading 不过这个方法内部会把LoadingValueReference变成对应引用类型的ValueReference )`
storeLoadedValue(key, hash, loadingValueReference, value);
return value;
} finally {
loader方法数据为null, 或者store数据异常, 删除LoadingValue
if (value == null) {
statsCounter.recordLoadException(loadingValueReference.elapsedNanos());
removeLoadingValue(key, hash, loadingValueReference);
}
}
}
Segment#storeLoadedValue():
boolean storeLoadedValue(
K key, int hash, LoadingValueReference<K, V> oldValueReference, V newValue) {
lock();
try {
long now = map.ticker.read();
preWriteCleanup(now);
int newCount = this.count + 1;
if (newCount > this.threshold) { // ensure capacity
expand();
newCount = this.count + 1;
}
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
int index = hash & (table.length() - 1);
ReferenceEntry<K, V> first = table.get(index);
// 找到entry
for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
ValueReference<K, V> valueReference = e.getValueReference();
V entryValue = valueReference.get();
// replace the old LoadingValueReference if it's live, otherwise
// perform a putIfAbsent
if (oldValueReference == valueReference
|| (entryValue == null && valueReference != UNSET)) {
++modCount;
if (oldValueReference.isActive()) {
RemovalCause cause =
(entryValue == null) ? RemovalCause.COLLECTED : RemovalCause.REPLACED;
enqueueNotification(key, hash, entryValue, oldValueReference.getWeight(), cause);
newCount--;
}
// LoadingValueReference变成对应引用类型的ValueReference,并进行赋值操作
setValue(e, key, newValue, now);
this.count = newCount; // write-volatile
evictEntries(e);
return true;
}
// the loaded value was already clobbered
enqueueNotification(key, hash, newValue, 0, RemovalCause.REPLACED);
return false;
}
}
++modCount;
ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
setValue(newEntry, key, newValue, now);
table.set(index, newEntry);
this.count = newCount; // write-volatile
evictEntries(newEntry);
return true;
} finally {
unlock();
// 清理
postWriteCleanup();
}
}
总结一下get方法的过程:
- 指定segment中table中找到没有被回收、没有过期的entry, 如果找到了, 并且在CacheBuilder配置了refreshAfterWrite, 并且当前时间已经超过了这个时间, 则重新加载. 否则, 返回这个值.
- 找到的ValueReference是loadingValueReference(正在加载). 此时waitForLoadingValue阻塞等待.
- 如果没有找到entry, 或者找到为null/expired. lockedGetOrLoad, 加锁. 在table里面找对应key的entry,
如果找到valueReference.isLoading() == true为正在加载中, 等待加载完, 拿到value值即可.
如果找到值为非null&非expired, 返回. 否则, 创建一个LoadingValueReference, 调用loadSync真正加载相关的值入table. - loadSync会执行loader, 然后给出ListenableFuture, 根据future去存储getAndRecordStats并且获取值. 存储storeLoadedValue.
然后, 大部分情况下替换LoadingValueReference, 少部分情况是删除LoadingValueReference(此时 是因为load方法执行得到的数据是null, 抛出了异常.)
下面是get的大体流程图:
2021年8月1日14点:
put
下面我们来介绍一下 put源码, !!! —重要
@Nullable
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
long now = map.ticker.read();
// 1. 先去清理key/value 引用队列, 再调用expireEntries 删除writeQueue/accessQueue中过期的entry
preWriteCleanup(now);
int newCount = this.count + 1;
if (newCount > this.threshold) { // ensure capacity 扩容
expand();
newCount = this.count + 1;
}
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
int index = hash & (table.length() - 1);
ReferenceEntry<K, V> first = table.get(index);
// Look for an existing entry.
for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
// We found an existing entry.
ValueReference<K, V> valueReference = e.getValueReference();
V entryValue = valueReference.get();
if (entryValue == null) {
++modCount;
// 2. value被自动删除, 因为值被回收了
if (valueReference.isActive()) {
enqueueNotification(
key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
setValue(e, key, value, now);
newCount = this.count; // count remains unchanged
} else {
setValue(e, key, value, now);
newCount = this.count + 1;
}
this.count = newCount; // write-volatile
// 3. 判断权重是否超了, 超了就要被删除
evictEntries(e);
return null;
// 4. 如果是存在&onlyIfAbsent==true
} else if (onlyIfAbsent) {
// Mimic
// "if (!map.containsKey(key)) ...
// else return map.get(key);
// 5. 记录被读取, 然后返回值
recordLockedRead(e, now);
return entryValue;
} else {
// clobber existing entry, count remains unchanged
++modCount;
enqueueNotification(
key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
setValue(e, key, value, now);
evictEntries(e);
return entryValue;
}
}
}
// Create a new entry.
++modCount;
ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
setValue(newEntry, key, value, now);
table.set(index, newEntry);
newCount = this.count + 1;
this.count = newCount; // write-volatile
// 每次写入之后都会判断权重, 是否删除
evictEntries(newEntry);
return null;
} finally {
unlock();
postWriteCleanup();
}
}
public V put(K key, V value); // onlyIfAbsent为fale
public V putIfAbsent(K key, V value); // onlyIfAbsent为true 这个参数的意思是如果缓存中没有才会写入数据哦
下面是put的大概流程:
preWriteCleanup
void preWriteCleanUp(long now)
传入当前时间
作用: 清理keyQueue和valueQueue中被GC, 等待清除的entry信息.
过期掉 writeQueue和accessQueue中entry.
evictEntries
void evictEntries(ReferenceEntry<K, V> newest);
传入参数为最新的entry, 可能是最新插入的, 也可能是刚更新过的.
这个方法只有设置了maximumSize才会进行.
作用/步骤: 1. 清除recencyQueue, 判断该元素自身的权重是否超过上限, 如果超过则移除元素.
2. 判断总的权重是否大于上限, 如果超过则去accessQueue找到队首(即最不常访问元素)进行移除, 直到小于上限.
remove
public V remove(@Nullable Object key);
调用LocalManualCache的invalidate(Object key)方法即可调用remove.
refresh 其实底层就是同步执行了load方法
数据结构
recencyQueue和accessQueue
如果在build缓存时设置了expiredAfterAccess || maxWeight, recencyQueue于accessQueue就会被初始化.
recencyQueue是最近读队列, accessQueue是最近访问队列(读写). 其中最近读队列是采用的是jdk中线程安全、支持高并发读写的ConcurrentLinkedQueue, 队列中存储的元素是ReferenceEntry. 最近访问队列采用的是LoadingCache自己实现的AccessQueue, 节点是ReferenceEntry、非线程安全. (带头结点的双向链表)
用两个队列实现LRU的原因为:
我们首先来看一下缓存下为啥要用LRU:
- 缓存的场景基本是读多写少, LoadingCache的读操作要做到高性能、lock-free的读, 这样就会有多个线程同时读缓存, 意味着LRU队列支持多线程高并发写入(调整元素在LRU队列中的访问顺序)
- LoadingCache中元素可能会过期、容量限制、被gc回收等原因被淘汰出缓存, 意味着需要从LRU队列中高效删除元素. 因此我们需要一个支持多线程并发访问的、常数时间删除元素的队列实现.
显然一个ConcurrentLinkedQueue不能同时满足这两个需求, Guava给的解就是再增加一个简单的AccessQueue做到常数时间删除元素. 具体来说:
- 每次无锁的读操作都会去写而且只会写最近读队列(将entry直接入队, 方法: recordRead)
- 每次锁保护下写操作都会涉及到最近访问队列的读写,
- 比如每次想缓存新增元素都会做几次清理工作. 清理就需要读accessQueue(淘汰掉队头的元素, 见方法写前expiredEntries, 写后evictEntries);
- 每次向缓存新增元素成功后记录元素写操作, 记录会写accessQueue(加到队尾, 见方法recordWrite).
- 每次访问accessQueue前都需要先排干recencyQueue至accessQueue中(key相同的, 按先进先出顺序, ==批量调整accessQueue中元素顺序), 然后再去进行accessQueue的读或者写操作, 以尽量保证accessQueue中元素顺序和真实的最近访问顺序一致. (见方法: drainRecencyQueue)
关于这个做法其实是有以下几个问题:
- 如果在写少读非常多的场景下, 读写accessQueue的机会很少, 大量读操作会在recencyQueue中累积很多元素占用内存而得不到排干的机会. 所以Guava为了解决这个难问题, 为读操作设置了一个DRAIN_THRESHOLD == 63(方法: postReadCleanup), 当累积读次数达到排干阈值时64也会触发一次清理操作, 从而排干recenceyQueue到accessQueue
- 在高并发场景下, 即使是在每次读写accessQueue前做排干recencyQueue操作, 也不能保证accessQueue中元素顺序和实际元素访问顺序一致. 这个问题也没有那么严重,导致结果无非就是LRU淘汰过程没那么deterministic, 对于一个缓存来说也可以接受.
writeQueue
最近写队列: 如果build缓存时设置expireAfterWrite, 创建segment时就会初始化writeQueue。
实现: 十分简单, 就是一个带头节点的双向循环链表, 并且没有任何并发. 节点对象就是ReferenceEntry本身.
注意到segment中hash表里的散列链表节点也是ReferenceEntry, 这就有一个技巧了: 即一个节点对象可能会同时属于多个链表中, 不同链表使用不同的前后节点指针, 这个技巧的好处在于给定一个节点entry对象, 所有链表都可以做到常数时间的查找和删除(jdk里面的linkedHashMap实现也采用了这个技巧).
由于写操作都会在锁保护进行, 因此这个writeQueue无需是线程安全的.
缓存回收
一个残酷的现实是, 我们好像没有足够的内存缓存所有数据. 你必须决定: 什么时候某个缓存项不值得保留了? Guava Cache 提供了三种基本的缓存回收方式: 基于容量回收、定时回收和基于引用回收.
基于容量回收
如果要规定缓存项的数目不超过固定值, 只需使用CacheBuilder.maximumSize(long). 缓存将尝试回收最近没有使用或者总体上没有使用的缓存项. ----- 注意: 在缓存项的数目达到限定值之前, 缓存就可能进行回收操作— 通常来说, 这种情况发生在缓存项的数目逼近限定值时.
另外, 不同缓存项有不同的权重(weights) --例如, 如果你的缓存值, 占据完全不同的内存空间, 你可以使用CacheBuilder.weight(Weighter)指定一个权重函数, 并可以用CacheBuilder.maximumWeight(long)指定最大总重.
定时回收
CacheBuilder提供了两种定时回收的方法:
- expiredAfterAccess(long, TimeUnit): 没有在给定时间内访问(读/写), 回收
- expiredAfterWrite(long, TimeUnit): 没有在给定时间内写, 回收
如下文: 定时回收大多是情况是在写进行, 偶尔在读进行
基于引用回收
通过使用弱引用的键/值, 软值. Cuava Cache可以把缓存设置为允许垃圾回收:
- CacheBuilder.weakKeys():
- CacheBuilder.weakValues():
- CacheBuilder.softKeys():
显示清除
任何时候, 我们都可以显式地清除缓存项, 而不是等着触发回收/被回收:
- 单个清除: Cache.invalidate(key)
- 批量清除: Cache.invalidateAll(key)
- 清除所有缓存项: Cache.invalidateAll()
移除监听器
通过CacheBuilder.removeListener(RemovalListener), 你可以定义一个监听器, 方便在缓存项被删除的时候做一些额外操作, removalListener,会获得移除通知removalNotification, 其中包含移除原因removalCause, 键/值.
.removalListener((RemovalListener<String, Long>) notification -> {
System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
})
注意: 默认情况下, 监听器方法是在移除缓存时同步调用的. 因为缓存的维护和请求响应通常是同时进行的, 代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求. 此时, 可以使用removalListeners.asynchronous(removalListener, executor)把监听器装饰为异步操作. --------------- 有待观察, 因为没看过底层
清理什么时候发生!! 重要!!
使用CacheBuilder构建的缓存是不会自动执行清理和回收工作, 也不会在某个缓存项过期后马上清理, 也没有诸如此类的清理机制. 相反, 它会在写操作时顺带做少量的维护工作, perWriteCleanUp->expiredEntries(write, accessQueue), evictEntries, postCleanUp, 或者偶尔在读操作时做—如果写操作实在是太少了.
这样做的原因是: 如果要自动地持续清理缓存, 就必须有一个线程, 这个线程会和用户线程竞争共享锁. 此外, 某些环境下线程创建可能受限制, 这样CacheBuilder其实就不太好用了.
其实这样做挺好的, 清理选择权交给自己手里, 可以手动Cache.cleanUp. ScheduledExecutorService可以帮助你更好的实现定时调度.
刷新
刷新和回收不太一样. 正如loadingCache.refresh(K)所说, 其实就是为键加载值, 这个过程其实可以是异步的. 在刷新操作进行时, 缓存仍然可以向其他线程返回旧值, 而不像回收操作, 读缓存的的线程必须等待新值加载完成.
如果刷新过程抛出异常, 缓存将保留旧值, 而异常会记录到日志后被丢弃[swallowed].
重载CacheLoader.reload(K, V)可以扩展刷新时的行为, 这个方法允许开发者在计算新值时使用旧的值. 废话. 底层其实就是同步执行了load方法, 不过其他线程refresh访问时, 有旧值返回.
下面是异步线程:
final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
private static final LoadingCache<String, Long> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(8, TimeUnit.SECONDS)
.build(new CacheLoader<String, Long>() {
@Override
public Long load(String key) {
long timeMillis = System.currentTimeMillis();
System.out.println("load, time=" + timeMillis);
return timeMillis;
}
@Override
public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
// asynchronous!
ListenableFutureTask<Long> task =
ListenableFutureTask.create(new Callable<Long>() {
@Override
public Long call() throws Exception {
long timeMillis = System.currentTimeMillis();
System.out.println(Thread.currentThread().getName() + " reload, time= " + timeMillis);
// 其实就是异步线程太快了, 以至于10s时有部分线程请求到了新值, 我们可以人为让异步线程放慢, 这时, 全部线程都会拿到旧值.
Thread.sleep(TimeUnit.SECONDS.toMillis(2));
return timeMillis;
}
});
service.execute(task);
return task;
}
});
main开始请求:
new Thread(() -> {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(0));
graphs.get(key1);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
Long a1 = graphs.get(key1);
System.out.println(Thread.currentThread().getName() + " a1 " + a1);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(12));
Long a2 = graphs.get(key1);
System.out.println(Thread.currentThread().getName() + " a2 " + a2);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}).start();
// a1, a2 都拿到的是旧值
注意: CacheBuilder.refreshAfterWrite(long, TimeUnit) 可以为缓存定时自动的刷新缓存. 缓存项只有被检索/访问时才会真正刷新, 这一点就不在赘述了. 上面有原因.
其他特性
统计
CacheBuilder.recordStats() 用来开启Guava Cache统计功能. 统计打开后, Cache.stats() 方法会返回CacheStats 对象以提供如下统计信息:
- hitRate(): 缓存命中率;
- averageLoadPenalty(): 加载新值的平均时间, 单位为ns.
- evictionCount(): 缓存项被回收的总数, 不包括显示清楚.
此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议
密切关注这些数据。
adMap视图
asMap 视图提供了缓存的ConcurrentMap 形式, 但asMap视图和缓存交互需要注意:
- cache.asMap() 包含当前所有加载到缓存的项. 因此相应地, cache.asMap().keySet() 包含当前所有已加载键;
- asMap().get(key) 实质上等同于cache.getIfPresent(key), 而且不会引起缓存项的加载. 这和map的约定是一致的.
- 所有读写操作都会重置相关缓存项的访问时间, cache.asMap().get, put, 但是不包括.containsKey(), 还有一些对Cache.asMap()的集合视图上的操作, 比如asMap().entrySet()不会重置缓存项的读取时间.
中断
待处理
refreshAfterWrite 和expiredAfterWrite 一般怎样设置/关系, 业务
四种引用
- 强引用: 创建一个对象并给它赋值一个引用, 引用是存在于jvm的栈中.
- 软引用: 如果一个对象具有软引用, 内存空间足够, 不回收, 否则, 回收. 看内存空间自己的度量.
- 弱引用: 垃圾回收器才不管你内存空间够不够, 直接回收. 看垃圾回收器心情.
- 虚引用: 虚引用其实和上面的软引用和弱引不同, 它并不影响对象的生命周期. 如果一个对象与虚引用关联, 则跟没有引用之前一样, 在任何时候都可能被垃圾回收器回收.
注: 虚引用(PhantomReference)的必须和引用队列(ReferenceQueue)一起使用, 当垃圾回收器准备回收一个对象时, 如果发现它还有虚引用, 就会把这个虚引用加入引用队列. 程序可以通过判断引用队列中是否加入了虚引用, 来了解被引用的对象是否将要被垃圾回收. 如果程序发现某个虚引用已经被加入了引用队列, 那么就可以在所引用的对象的内存被回收之前采取必要的行动.
异步线程 (看他源码)
- 怎样创建一个异步线程
final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFutureTask<Long> task =
ListenableFutureTask.create(new Callable<Long>() {
@Override
public Long call() throws Exception {
return timeMillis;
}
});
service.execute(task);
return task;
- feture->listenFeture
concurrentHashMap1.7 -> 1.8
本文参考:
https://blog.csdn.net/zjccsg/article/details/51932252
https://www.jianshu.com/p/38bd5f1cf2f2
http://www.qishunwang.net/knowledge_show_177365.aspx