java截取固定大小图片_java缓存(三)——实现一个固定大小的对象缓存池

本文将介绍使用java语言实现一个对象缓存池。一步步的实现包括高速命中,固定大小的缓存队列等功能。

这一期我们终于能够动手编写一些代码,使用java来实现一个在内存中的对象缓存池。

不限大小的高速缓存池

最开始的需求是实现一个能够在单线程模式下,根据唯一主键key来缓存对象的功能。

对于java的集合类来说,能够得到近似的存取时间复杂度为O(1)的数据结构就是HashMap了,此处我们不再讲述其数据结构实现,简单的一段代码实现此功能:

public class ObjectCache {

private Map cache;

public ObjectCache() {

cache = new HashMap<>();

}

public void put(String key, Object value) {

cache.put(key, value);

}

public Object get(String key) {

return cache.get(key);

}

}

限制大小的高速缓存池

JVM的堆内存大小是有限的,如果一个缓存没有退出机制,永远只能往里面加入对象的话,那么最终就会导致堆内存溢出错误。所以一般来说我们都要限制缓存池的大小,以免内存耗尽。

那么当缓存对象达到最大限制大小后,用什么机制来淘汰过期的缓存对象呢?常用的有如下策略:

FIFO

此策略根据写入的时间排序,当需要淘汰时,首先淘汰最早写入的对象。

LRU

此策略根据最后读取的时间排序,当需要淘汰时,首先淘汰最后读取时间最早的对象。

LFU

此策略根据一段时间窗口内,总的读取次数排序,当需要淘汰时,首先淘汰时间窗口内读取次数最少的对象。

其中LFU实现比较复杂,需要使用滑窗计数器来帮助实现,后续会单独一篇文章来介绍此算法。这次我们先来了解比较简单的FIFO和LRU算法的实现。

我们最开始使用的HashMap是无序的,所以无法单独来实现读取或者写入的排序。我们考虑此场景,FIFO需要每次写入或者更新的时候都改变排序,而LRU每次读取的时候要改变排序,所以我们就需要一个能够排序的,而且很快速改变某个节点位置的数据结构。那么当然我们会想到LinkedList链表数据结构,其插入节点的时间复杂度为O(1)且能够保持节点次序,但是单独的LinkedList的查询时间复杂度又是O(N),超出我们预期。所以此处我们需要将其结合使用,在使用HashMap提供高速查询写入的同时,又使用LinkedList来维护其插入或者最后读取的次序,同时我们在HashMap和LinkedList里维护同一个对象的引用,这样整体的存储空间保持基本不变。

其实JDK在1.7之后已经为我们提供了这样的数据结构:java.util.LinkedHashMap

LinkedHashMap直接继承自HashMap类,同时在内部维护了一个双向链表,其实现为:

8cf9334cc728

image.png

可以看到其内部的链表类LinkedHashMap.Entry继承自HashMap.Node,同时也实现了Map.Entry接口,这样就能在直接在链表中使用HashMap中的Node对象,从而保持同一个对象引用。

在LinkedHashMap.Entry类中,其before和after属性分别指向当前节点的前节点和后节点,而LinkedHashMap中也通过属性head和tail维护了此链表的头节点和尾节点:

/**

* The head (eldest) of the doubly linked list.

*/

transient LinkedHashMap.Entry head;

/**

* The tail (youngest) of the doubly linked list.

*/

transient LinkedHashMap.Entry tail;

LinkedHashMap通过重写了HashMap中创建节点的一些方法来在新增节点时维护链表数据:

Node newNode(int hash, K key, V value, Node e) {

LinkedHashMap.Entry p =

new LinkedHashMap.Entry(hash, key, value, e);

linkNodeLast(p);

return p;

}

Node replacementNode(Node p, Node next) {

LinkedHashMap.Entry q = (LinkedHashMap.Entry)p;

LinkedHashMap.Entry t =

new LinkedHashMap.Entry(q.hash, q.key, q.value, next);

transferLinks(q, t);

return t;

}

TreeNode newTreeNode(int hash, K key, V value, Node next) {

TreeNode p = new TreeNode(hash, key, value, next);

linkNodeLast(p);

这里可以看到,在HashMap的EntrySet数组中,根据hash碰撞的命中数量,采用链表和红黑树两种节点结构(JDK1.8以后),分别用newNode和newTreeNode插入节点,每次都把新的节点放在LinkedHashMap双向链表的最末尾。

同时我们来看HashMap中,在1.7之后为了LinkedHashMap提供了三个回调方法,其在HashMap中的默认实现为空:

// Callbacks to allow LinkedHashMap post-actions

// 访问节点的值后调用

void afterNodeAccess(Node p) { }

// 插入新的节点后调用

void afterNodeInsertion(boolean evict) { }

// 删除节点后调用

void afterNodeRemoval(Node p) { }

而LinkedHashMap在继承HashMap后重写这三个方法:

// 删除节点后被HashMap回调

void afterNodeRemoval(Node e) { // unlink

LinkedHashMap.Entry p =

(LinkedHashMap.Entry)e, b = p.before, a = p.after;

p.before = p.after = null;

if (b == null)

head = a;

else

b.after = a;

if (a == null)

tail = b;

else

a.before = b;

}

// 插入新的节点后被HashMap回调

void afterNodeInsertion(boolean evict) { // possibly remove eldest

LinkedHashMap.Entry first;

if (evict && (first = head) != null && removeEldestEntry(first)) {

K key = first.key;

removeNode(hash(key), key, null, false, true);

}

}

// 访问节点的值后被HashMap回调

void afterNodeAccess(Node e) { // move node to last

LinkedHashMap.Entry last;

if (accessOrder && (last = tail) != e) {

LinkedHashMap.Entry p =

(LinkedHashMap.Entry)e, b = p.before, a = p.after;

p.after = null;

if (b == null)

head = a;

else

b.after = a;

if (a != null)

a.before = b;

else

last = b;

if (last == null)

head = p;

else {

p.before = last;

last.after = p;

}

tail = p;

++modCount;

}

}

可以看到其中主要就是一些经典的链表操作,更新其次序;我们注意到accessOrder属性,其为true后才会在访问节点对象后更新其次序,我们来看其在LinkedHashMap中的定义:

/**

* The iteration ordering method for this linked hash map: true

* for access-order, false for insertion-order.

*

* @serial

*/

final boolean accessOrder;

也就是当accessOrder为true时链表采用访问次序排序,为false时采用插入次序排序。其值在LinkedHashMap的构造函数中写入:

public LinkedHashMap(int initialCapacity,

float loadFactor,

boolean accessOrder) {

……

}

我们再来看afterNodeInsertion回调方法中调用的方法:removeEldestEntry,其默认实现永远返回false,那么就是说其实LinkedHashMap不会自动删除过期节点,需要我们自己继承后实现。

好了,既然如此,我们就来继承它来实现一个固定大小的对象缓存池吧:

public class FixedSizeCache extends LinkedHashMap {

/**

* 缓存池的最大大小

*/

private int maxSize = 0;

public FixedSizeCache(int initialCapacity,

float loadFactor,

boolean accessOrder,

int maxSize) {

super(initialCapacity, loadFactor, accessOrder);

this.maxSize = maxSize;

}

/**

* 当前缓存大小已经大于maxSize后返回true,在新增节点后会删除一个最老的节点

*

* @param eldest

* @return

*/

protected boolean removeEldestEntry(Map.Entry eldest) {

return size() > maxSize;

}

}

好了,如此简单,当accessOrder为true时就是一个LRU缓存池,当为false时就是一个FIFO缓存池。当然此缓存池不保证线程安全,只能在单线程下使用了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值