/*
* Copyright (c) 2013 Websquared, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v2.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*
* Contributors:
* swsong - initial API and implementation
*/
package org.fastcatsearch.util;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* memory low상황에서 회수되는 메모리를 사용하는 LRU캐시. 새로운 entry를 입력테스트시 1만건 입력에 600ms~1500ms의 시간소요. 검색엔진의 qps가 500이라고 가정하면 최대 75ms이 소요되므로 병목문제는 없음.
* 메모리도 60mb이내로 메모리 원활.
* */
public class LRUCache<K, V> {
/*
* CachedEntry는 soft ref이므로 memory low 상황에서 gc대상이된다. 내부 필드 CacheKey<K>는 Queue에 strong ref로 잡혀있지만,
* CachedEntry 객체 자체는 gc가 되고, CacheKey 객체는 유지 된다.
* */
private Queue<CacheKey<K>> lruQueue;
private Map<CacheKey<K>, SoftReference<CachedEntry<CacheKey<K>, V>>> map;
private ReentrantLock lock = new ReentrantLock();
private int maxCacheSize;
public LRUCache(int maxCacheSize) {
this.maxCacheSize = maxCacheSize;
lruQueue = new PriorityBlockingQueue<CacheKey<K>>(maxCacheSize);
map = new ConcurrentHashMap<CacheKey<K>, SoftReference<CachedEntry<CacheKey<K>, V>>>(maxCacheSize, 1.0f);
}
public void close() {
map.clear();
lruQueue.clear();
map = null;
lruQueue = null;
}
public void put(K key, V value) {
if (map == null) {
return;
}
lock.lock();
try{
if (value != null) {
if (map.size() >= maxCacheSize || lruQueue.size() >= maxCacheSize) {
// old를 지운다.
CacheKey<K> cacheKey = lruQueue.poll();
if (cacheKey != null) {
map.remove(cacheKey);
}
}
CacheKey<K> cacheKey = new CacheKey<K>(key);
CachedEntry<CacheKey<K>, V> e = new CachedEntry(cacheKey, value, map, lruQueue);
SoftReference<CachedEntry<CacheKey<K>, V>> ref = new SoftReference<CachedEntry<CacheKey<K>, V>>(e);
SoftReference<CachedEntry<CacheKey<K>, V>> oldRef = map.put(cacheKey, ref);
if (oldRef != null) {
if (oldRef.get() != null) {
lruQueue.remove(oldRef.get().key());
}
oldRef.clear();
}
lruQueue.offer(cacheKey);
// logger.debug("map[{}], Q[{}]", map.size(), lruQueue.size());
}
}finally{
lock.unlock();
}
}
public V get(K key) {
CacheKey<K> cacheKey = new CacheKey<K>(key);
// lock.lock();
try{
if(map.containsKey(cacheKey)) {
SoftReference<CachedEntry<CacheKey<K>, V>> ref = map.get(cacheKey);
if (ref != null) {
CachedEntry<CacheKey<K>, V> entry = ref.get();
if (entry != null) {
if(lruQueue.remove(cacheKey)){
//시간 업데이트.
lruQueue.offer(cacheKey);
//boolean b = lruQueue.offer(cacheKey);
//logger.debug("get update {} > {}", b, cacheKey);
}
// 아직 회수안된 객체이다.
return entry.value();
}
}
}
}finally{
// lock.unlock();
}
return null;
}
public int size() {
// logger.debug("m[{}], q[{}]", map.size(), lruQueue.size());
return map.size();
}
}
class CacheKey<K> implements Comparable<CacheKey<K>> {
private K key;
private long lastUsedTime;
public CacheKey(K key){
this.key = key;
lastUsedTime = System.nanoTime();
}
@Override
public String toString(){
return key.toString() + "[" + lastUsedTime +"]";
}
public long lastUsedTime() {
return lastUsedTime;
}
@Override
public int compareTo(CacheKey<K> o) {
if (lastUsedTime - o.lastUsedTime < 0) {
return -1;
} else if (lastUsedTime - o.lastUsedTime > 0) {
return 1;
} else {
return 0;
}
}
@Override
public boolean equals(Object o){
CacheKey<K> e = (CacheKey<K>) o;
return key.equals(e.key);
}
@Override
public int hashCode(){
return key.hashCode();
}
}
class CachedEntry<CK, V> {
protected static final Logger logger = LoggerFactory.getLogger(CachedEntry.class);
private CK key;
private V value;
private Queue<CK> lruQueue;
private Map<CK, SoftReference<CachedEntry<CK, V>>> ref;
public CachedEntry(CK key, V value, Map<CK, SoftReference<CachedEntry<CK, V>>> ref, Queue<CK> lruQueue) {
this.key = key;
this.value = value;
this.ref = ref;
this.lruQueue = lruQueue;
}
public CK key() {
return key;
}
public V value() {
return value;
}
/*
* soft reference의 메모리 회수시 각 map과 q에서도 자신을 삭제하도록 한다.
*/
@Override
public void finalize() {
Object o = ref.remove(key);
boolean b = lruQueue.remove(key);
// logger.debug("finalize {} > map[{}], q[{}], {}, {}", key.toString(), ref.size(), lruQueue.size(), o, b);
}
@Override
public String toString() {
return key.toString() + ">" + value.toString();
}
}