前言
在多線程高併發場景中往往是離不開cache的,需要根據不同的應用場景來需要選擇不同的cache,比如分佈式緩存如redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCache。之前用spring cache的時候集成的是ehcache,但接觸到GuavaCache之後,被它的簡單、強大、及輕量級所吸引。它不需要配置文件,使用起來和ConcurrentHashMap一樣簡單,而且能覆蓋絕大多數使用cache的場景需求!
GuavaCache是google開源java類庫Guava的其中一個模塊,在maven工程下使用可在pom文件加入如下依賴:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
Cache接口及其實現
先說說一般的cache都會實現的基礎功能包括:
提供一個存儲緩存的容器,該容器實現了存放(Put)和讀取(Get)緩存的接口供外部調用。 緩存通常以<key,value>的形式存在,通過key來從緩存中獲取value。當然容器的大小往往是有限的(受限於內存大小),需要爲它設置清除緩存的策略。
在GuavaCache中緩存的容器被定義爲接口Cache<K, V>的實現類,這些實現類都是線程安全的,因此通常定義爲一個單例。並且接口Cache是泛型,很好的支持了不同類型的key和value。作爲示例,我們構建一個key爲Integer、value爲String的Cache實例:
final static Cache<Integer, String> cache = CacheBuilder.newBuilder()
//設置cache的初始大小爲10,要合理設置該值
.initialCapacity(10)
//設置併發數爲5,即同一時間最多只能有5個線程往cache執行寫入操作
.concurrencyLevel(5)
//設置cache中的數據在寫入之後的存活時間爲10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
//構建cache實例
.build();
據說GuavaCache的實現是基於ConcurrentHashMap的,因此上面的構造過程所調用的方法,通過查看其官方文檔也能看到一些類似的原理。比如通過initialCapacity(5)定義初始值大小,要是定義太大就好浪費內存空間,要是太小,需要擴容的時候就會像map一樣需要resize,這個過程會產生大量需要gc的對象,還有比如通過concurrencyLevel(5)來限制寫入操作的併發數,這和ConcurrentHashMap的鎖機制也是類似的(ConcurrentHashMap讀不需要加鎖,寫入需要加鎖,每個segment都有一個鎖)。
接下來看看Cache提供哪些方法(只列了部分常用的):
/**
* 該接口的實現被認爲是線程安全的,即可在多線程中調用
* 通過被定義單例使用
*/
public interface Cache<K, V> {
/**
* 通過key獲取緩存中的value,若不存在直接返回null
*/
V getIfPresent(Object key);
/**
* 通過key獲取緩存中的value,若不存在就通過valueLoader來加載該value
* 整個過程爲 "if cached, return; otherwise create, cache and return"
* 注意valueLoader要麼返回非null值,要麼拋出異常,絕對不能返回null
*/
V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
/**
* 添加緩存,若key存在,就覆蓋舊值
*/
void put(K key, V value);
/**
* 刪除該key關聯的緩存
*/
void invalidate(Object key);
/**
* 刪除所有緩存
*/
void invalidateAll();
/**
* 執行一些維護操作,包括清理緩存
*/
void cleanUp();
}
使用過程還是要認真查看官方的文檔,以下Demo簡單的展示了Cache的寫入,讀取,和過期清除策略是否生效:
public static void main(String[] args) throws Exception {
cache.put(1, "Hi");
for(int i=0 ;i<100 ;i++) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date())
+ " key:1 ,value:"+cache.getIfPresent(1));
Thread.sleep(1000);
}
}
清除緩存的策略
基於存活時間的清除(Timed Eviction)
這應該是最常用的清除策略,在構建Cache實例的時候,CacheBuilder提供兩種基於存活時間的構建方法:基於容量的清除(size-based eviction)
顯式清除
(1)個別清除:Cache.invalidate(key)
(2)批量清除:Cache.invalidateAll(keys)
(3)清除所有緩存項:Cache.invalidateAll()
基於引用的清除(Reference-based Eviction)
在構建Cache實例過程中,通過設置使用弱引用的鍵、或弱引用的值、或軟引用的值,從而使JVM在GC時順帶實現緩存的清除,不過一般不輕易使用這個特性。清除什麼時候發生?
public class CacheService {
static Cache<Integer, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.SECONDS)
.build();
public static void main(String[] args) throws Exception {
new Thread() { //monitor
public void run() {
while(true) {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println(sdf.format(new Date()) +" size: "+cache.size());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
};
}.start();
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
cache.put(1, "Hi");
System.out.println("write key:1 ,value:"+cache.getIfPresent(1));
Thread.sleep(10000);
// when write ,key:1 clear
cache.put(2, "bbb");
System.out.println("write key:2 ,value:"+cache.getIfPresent(2));
Thread.sleep(10000);
// when read other key ,key:2 do not clear
System.out.println(sdf.format(new Date())
+" after write, key:1 ,value:"+cache.getIfPresent(1));
Thread.sleep(2000);
// when read same key ,key:2 clear
System.out.println(sdf.format(new Date())
+" final, key:2 ,value:"+cache.getIfPresent(2));
}
}
00:34:17 size: 0
write key:1 ,value:Hi
00:34:19 size: 1
00:34:21 size: 1
00:34:23 size: 1
00:34:25 size: 1
write key:2 ,value:bbb
00:34:27 size: 1
00:34:29 size: 1
00:34:31 size: 1
00:34:33 size: 1
00:34:35 size: 1
00:34:37 after write, key:1 ,value:null
00:34:37 size: 1
00:34:39 final, key:2 ,value:null
00:34:39 size: 0
(3)發生讀操作cache.getIfPresent(1)後,緩存項<2,"bbb">沒有被清除,因爲還是size=1,看來讀操作確實不一定會發生清除