内存缓存GuavaCache源码解析
来源:互联网 发布:鬼武者 知乎 编辑:程序博客网 时间:2024/06/09 15:51
1、前言
Guava Cache是一个全内存的本地缓存,它提供了线程安全的实现机制。其简单易用、性能好是本地缓存的不二之选。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地Guava Cache为了限制内存占用,通常都设定为自动回收元素。在某些场景下,尽管LoadingCache不回收元素,但考虑到其会自动加载缓存,在某些场景也是很有用的。
Guava Cache适用于消耗内存空间来提升速度、某些键会被查询一次以上、缓存中存放的数据总量不会超出内存容量的应用场景。Guava Cache是单个应用运行时的本地缓存,它不把数据存放到文件或外部服务器,如果要得到持久缓存可以尝试使用Redis或者Memcached。
缓存是指暂时在内存中保存某些数据的处理结果,等待下次访问可以快速取出使用。在日常开发中,受限于IO性能或自身业务系统的数据处理能力,获取数据可能非常费时。系统某些数据请求量很大、频繁IO、重复逻辑处理,都会导致系统出现瓶颈。缓存则可以较好解决这样的问题。缓存的作用是将这些数据保存在内存中,当有其它线程或客户端需相同数据时,则可直接从缓存内存块中取出数据。这样,不但可以提高系统的响应时间,也可以节省处理数据的资源消耗,整体上来系统性能会有大大的提升。
2、源码分析
本文以Guava-18.0.jar中源码为例进行阐述。
通常情况下,Guava cache在程序中会以com.google.common.cache.Cache或com.google.common.cache.LoadingCache的形式出现。Cache是接口,LoadingCache也是接口,不过其继承了Cache。
public interface Cache<K, V> {}public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {}
它们共有的方法都存在Cache中,如下所示:
/** * Returns the value associated with {@code key} in this cache, or {@code null} if there is no */ @Nullable V getIfPresent(Object key); /** * Returns the value associated with {@code key} in this cache, obtaining that value from */ V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; /** * Returns a map of the values associated with {@code keys} in this cache. */ ImmutableMap<K, V> getAllPresent(Iterable<?> keys); /** * Associates {@code value} with {@code key} in this cache. If the cache previously contained a value associated with {@code key}, the old value is replaced */ void put(K key, V value); /** * Copies all of the mappings from the specified map to the cache */ void putAll(Map<? extends K,? extends V> m); /** * Discards any cached value for key {@code key}. */ void invalidate(Object key); /** * Discards any cached values for keys {@code keys}. */ void invalidateAll(Iterable<?> keys); /** * Discards all entries in the cache. */ void invalidateAll(); /** * Returns the approximate number of entries in this cache. */ long size(); /** * Returns a current snapshot of this cache's cumulative statistics. */ CacheStats stats(); /** * Returns a view of the entries stored in this cache as a thread-safe map. */ ConcurrentMap<K, V> asMap(); /** * Performs any pending maintenance operations needed by the cache. */ void cleanUp();
比较常用的方法有:
V getIfPresent(Object key); V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; void put(K key, V value); void putAll(Map<? extends K,? extends V> m);
Cacahe是接口我们无法使用,Guava中提供了创建者CacheBuilder可以创建一个Cache,实现如下:
Cache<String, Person> caches = CacheBuilder.newBuilder(). // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项 maximumSize(100). // 设置写缓存后8秒钟过期 expireAfterWrite(8, TimeUnit.SECONDS). // 设置缓存容器的初始容量为10 initialCapacity(10). // 当缓存中个数大于设置个数时就会移除 removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { System.out.println( notification.getKey() + " was removed, cause is " + notification.getCause()); } }).build();//其中最后带有参数的build实现//在缓存不存在时通过CacheLoader的实现自动加载缓存.build(new CacheLoader<Integer, Student>() { @Override public Student load(Integer key) throws Exception { System.out.println("loading student " + key); Student student = new Student(); student.setId(key); student.setName("name " + key); return student; } });
CacheBuilder使用了创建者模式,对于内部的成员变量赋值,它都提供了对应的构建方法,代码如下所示:
/** //直接new了一个CacheBuilder * Constructs a new {@code CacheBuilder} instance with default settings, including strong keys, * strong values, and no automatic eviction of any kind. */ public static CacheBuilder<Object, Object> newBuilder() { return new CacheBuilder<Object, Object>(); }//例如设置Cache缓存最大容量 public CacheBuilder<K, V> maximumSize(long size) { checkState(this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize); checkState(this.maximumWeight == UNSET_INT, "maximum weight was already set to %s", this.maximumWeight); checkState(this.weigher == null, "maximum size can not be combined with weigher"); checkArgument(size >= 0, "maximum size must not be negative"); this.maximumSize = size; return this; }//设置写入缓存多久失效 public CacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) { checkState(expireAfterWriteNanos == UNSET_INT, "expireAfterWrite was already set to %s ns", expireAfterWriteNanos); checkArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit); this.expireAfterWriteNanos = unit.toNanos(duration); return this; }
通过一系列的build方法初始化参数后,可以增加缓存被移除的监听器,当缓存数量大于设置的缓存大小时,就会根据LRU算法移除缓存项,代码如下所示:
@CheckReturnValue public <K1 extends K, V1 extends V> CacheBuilder<K1, V1> removalListener( RemovalListener<? super K1, ? super V1> listener) { checkState(this.removalListener == null); // safely limiting the kinds of caches this can produce @SuppressWarnings("unchecked") CacheBuilder<K1, V1> me = (CacheBuilder<K1, V1>) this; me.removalListener = checkNotNull(listener); return me; }
当参数和监听都设置完成后,最后调用不带参数的bulid()方法来创建Cache,也可以调用带参数的build(CacheLoader
//不带参数的build方法 public <K1 extends K, V1 extends V> Cache<K1, V1> build() { checkWeightWithWeigher(); checkNonLoadingCache(); return new LocalCache.LocalManualCache<K1, V1>(this); }//带参数的build方法 public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( CacheLoader<? super K1, V1> loader) { checkWeightWithWeigher(); return new LocalCache.LocalLoadingCache<K1, V1>(this, loader); }
我们发现两种不同的build方法分别是通过LocalCache的内部类LocalLoadingCache和LocalManualCache实现的。 其中带有参数的build方法可以指定CacheLoader,即在缓存不存在时通过CacheLoader的实现自动加载缓存。代码示例如下所示:
//LocalManualCache部分代码如下所示 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; } @Override @Nullable public V getIfPresent(Object key) { return localCache.getIfPresent(key); } @Override public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException { checkNotNull(valueLoader); return localCache.get(key, new CacheLoader<Object, V>() { @Override public V load(Object key) throws Exception { return valueLoader.call(); } }); }//LocalLoadingCache部分代码如下所示,LocalLoadingCache继承了LocalManualCache,实现了LoadingCache接口,故其比LocalManualCache提供了更多的功能。 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))); } // LoadingCache methods @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e.getCause()); } }
上面代码看出不管是LocalLoadingCache还是LocalManualCache,其底层都是对LocalCache进行封装,也可看做使用了变种的适配器模式,将LocalCache包装到LocalManualCache中,而LocalManualCache实现了Cache接口,让本来由LocalManualCache来完成的功能转移到让LocalCache去完成。LocalCache代码实现如下所示:
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {}
可以看出LocalCache继承了抽象的Map并实现了并发的Map,我们知道Cache本身就是一个Map,看到这里就很容易理解了。LocalCache具备Map所有特效,并且其还包含若干并发安全的方法,例如:
V putIfAbsent(K key, V value); boolean remove(Object key, Object value); V replace(K key, V value);
这些方法都是线程安全的。下面继续分析LocalCache中如常用的get方法和put方法。
//LocalCache//get方法 @Nullable public V getIfPresent(Object key) { int hash = hash(checkNotNull(key)); V value = segmentFor(hash).get(key, hash); if (value == null) { globalStatsCounter.recordMisses(1); } else { globalStatsCounter.recordHits(1); } return value; } V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException { int hash = hash(checkNotNull(key)); return segmentFor(hash).get(key, hash, loader); }//LocalCache//put方法 @Override public V put(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, false); } @Override public V putIfAbsent(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, true); } @Override public void putAll(Map<? extends K, ? extends V> m) { for (Entry<? extends K, ? extends V> e : m.entrySet()) { put(e.getKey(), e.getValue()); } }
可以看出不管get方法还是put方法,都是通过segmentFor方法来实现的,代码如下所示:
/** * Returns the segment that should be used for a key with the given hash. * * @param hash the hash code for the key * @return the segment */ Segment<K, V> segmentFor(int hash) { // TODO(fry): Lazily create segments? return segments[(hash >>> segmentShift) & segmentMask]; }
最终可以看出是通过segments数组进行操作。
/** The segments, each of which is a specialized hash table. */ final Segment<K, V>[] segments;
//下面是LocalCache类下面的注释说明 /* * The basic strategy is to subdivide the table among Segments, each of which itself is a * concurrently readable hash table. The map supports non-blocking reads and concurrent writes * across different segments. * * If a maximum size is specified, a best-effort bounding is performed per segment, using a * page-replacement algorithm to determine which entries to evict when the capacity has been * exceeded. * * The page replacement algorithm's data structures are kept casually consistent with the map. The * ordering of writes to a segment is sequentially consistent. An update to the map and recording * of reads may not be immediately reflected on the algorithm's data structures. These structures * are guarded by a lock and operations are applied in batches to avoid lock contention. The * penalty of applying the batches is spread across threads so that the amortized cost is slightly * higher than performing just the operation without enforcing the capacity constraint. * * This implementation uses a per-segment queue to record a memento of the additions, removals, * and accesses that were performed on the map. The queue is drained on writes and when it exceeds * its capacity threshold. * * The Least Recently Used page replacement algorithm was chosen due to its simplicity, high hit * rate, and ability to be implemented with O(1) time complexity. The initial LRU implementation * operates per-segment rather than globally for increased implementation simplicity. We expect * the cache hit rate to be similar to that of a global LRU algorithm. */
可以看出,LocalCache中的get、put等方法是通过Segment来实现的,它引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,即分成了N个Segment。都是现根据key.hashCode()算出放到哪个Segment中。(我想大多数程序员对ConcurrentMap还是比较熟悉的,关于其中详细的源码不在这里累赘,有疑问可以自行百度查询)
3、代码示例
- 测试Cache:
package guavacache;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import com.google.common.cache.Cache;import com.google.common.cache.CacheBuilder;import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;public class GuavaCache02 { private Cache<String, Person> caches; public GuavaCache02() { if (caches == null) { //双重锁判断 synchronized (this) { caches = CacheBuilder.newBuilder(). // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项 maximumSize(100). // 设置写缓存后8秒钟过期 expireAfterWrite(8, TimeUnit.SECONDS). // 设置缓存容器的初始容量为10 initialCapacity(10). // 当缓存中个数大于设置个数时就会移除 removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { System.out.println( notification.getKey() + " was removed, cause is " + notification.getCause()); } }).build(); caches.put("1", new Person("1", "zhangsan")); caches.put("2", new Person("2", "lisi")); caches.put("3", new Person("3", "wangwu")); } } } public static void main(String[] args) throws ExecutionException { GuavaCache02 cache = new GuavaCache02(); cache.testCache(); } public void testCache() throws ExecutionException { Person p1 = caches.getIfPresent("1"); Person p2 = caches.getIfPresent("2"); Person p3 = caches.getIfPresent("3"); System.err.println("all: " + p1 + " " + p2 + " " + p3); Thread thread = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub try { // 过期了 Thread.sleep(8000); System.err.println(caches.getIfPresent("1") + " Thread: " + Thread.currentThread().getName()); System.err.println(caches.getIfPresent("2") + " Thread: " + Thread.currentThread().getName()); System.err.println(caches.getIfPresent("3") + " Thread: " + Thread.currentThread().getName()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }); thread.start(); try { // 过期了 Thread.sleep(8000); System.err.println(caches.getIfPresent("1") + " Thread: " + Thread.currentThread().getName()); System.err.println(caches.getIfPresent("2") + " Thread: " + Thread.currentThread().getName()); System.err.println(caches.getIfPresent("3") + " Thread: " + Thread.currentThread().getName()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } }}
运行结果:
all: guavacache.Person@4d405ef7 guavacache.Person@6193b845 guavacache.Person@2e817b38null Thread: Thread-1null Thread: Thread-1null Thread: Thread-1null Thread: mainnull Thread: mainnull Thread: main
- 测试LoadingCache:
package guavacache;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import com.google.common.cache.CacheBuilder;import com.google.common.cache.CacheLoader;import com.google.common.cache.LoadingCache;import com.google.common.cache.RemovalListener;import com.google.common.cache.RemovalNotification;public class TestGuavaCache { public static void main(String[] args) throws ExecutionException, InterruptedException { // 缓存接口这里是LoadingCache,LoadingCache在缓存项不存在时可以自动加载缓存 LoadingCache<Integer, Student> studentCache // CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例 = CacheBuilder.newBuilder() // 设置并发级别为8,并发级别是指可以同时写缓存的线程数 .concurrencyLevel(8) // 设置写缓存后8秒钟过期 .expireAfterWrite(8, TimeUnit.SECONDS) // 设置缓存容器的初始容量为10 .initialCapacity(10) // 设置缓存最大容量为100,超过100之后就会按照LRU最近虽少使用算法来移除缓存项 .maximumSize(100) // 设置要统计缓存的命中率 .recordStats() // 设置缓存的移除通知 .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { System.out.println( notification.getKey() + " was removed, cause is " + notification.getCause()); } }) // build方法中可以指定CacheLoader,在缓存不存在时通过CacheLoader的实现自动加载缓存 .build(new CacheLoader<Integer, Student>() { @Override public Student load(Integer key) throws Exception { System.out.println("loading student " + key); Student student = new Student(); student.setId(key); student.setName("name " + key); return student; } }); for (int i = 0; i < 20; i++) { // 从缓存中得到数据,由于我们没有设置过缓存,所以需要通过CacheLoader加载缓存数据 Student student = studentCache.get(1); System.out.println(student); // 休眠1秒 TimeUnit.SECONDS.sleep(1); } System.out.println("cache stats: "); // 最后打印缓存的命中率等 情况 System.out.println(studentCache.stats().toString()); }}
运行结果:
loading student 1guavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135aguavacache.Student@27fa135a1 was removed, cause is EXPIREDloading student 1guavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35aguavacache.Student@7aec35a1 was removed, cause is EXPIREDloading student 1guavacache.Student@67424e82guavacache.Student@67424e82guavacache.Student@67424e82guavacache.Student@67424e82cache stats: CacheStats{hitCount=17, missCount=3, loadSuccessCount=3, loadExceptionCount=0, totalLoadTime=1114359, evictionCount=2}
4、总结
本文简单对Guava Cache源码实现进行剖析,并通过代码示例进一步理解其具体使用。其实例化使用简单流程如下:
构建CacheBuilder实例cacheBuilder
cacheBuilder实例指定缓存器LocalCache的初始化参数
cacheBuilder实例使用build()方法创建LocalCache实例
为各个类变量赋值(通过第二步中cacheBuilder指定的初始化参数以及原本就定义好的一堆常量)
创建Segment数组
初始化每一个Segment[i]
为Segment属性赋值
初始化Segment中的table,即一个ReferenceEntry数组(每一个key-value就是一个ReferenceEntry)
根据之前类变量的赋值情况,创建相应队列,用于LRU缓存回收算法
本文阐述了Guava Cache相关内容,由于个人能力有限,部分内容可能有所遗漏,欢迎大家能够指出。希望本文对你有所帮助。
- 内存缓存GuavaCache源码解析
- 内存缓存LruCache源码解析
- 本地缓存GuavaCache
- 一起读guavaCache源码
- android LruCache内存缓存源码解析
- Android内存缓存LruCache源码解析
- Android内存缓存管理LruCache源码解析与示例
- ImageLoader内存缓存解析
- Glide缓存源码解析
- [spark] RDD缓存源码解析
- OkHttp3源码解析03-缓存
- LruCache缓存类源码解析
- mongodb 源码解析内存管理
- 【内存优化】ArrayMap源码解析
- GuavaCache简介
- GuavaCache简介
- UniversalImageLoader源码解读04-内存缓存
- SAX 解析到文件,缓存到内存
- 生物信息学习——tophat使用手册
- Android基础学习笔记之-基本文件读写实现
- 344. Reverse String(C++)
- 使用@include-media写媒介查询
- 浅谈项目组wiki维护实现方案
- 内存缓存GuavaCache源码解析
- gulp运用
- Android基础系列之Activity
- ACM刷题之Codeforces ————String Game
- Boost库的timer类讲解
- 剑指offer面试题
- 220. Contains Duplicate III
- 神经网络weight参数怎么初始化
- JAVASE--03--继承