更新存储的Iterator时的ConcurrentModificationException(用于LRU缓存实现)

时间:2016-04-02 23:10:37

标签: java caching linked-list hashmap lru

我正在尝试实现自己的LRU缓存。是的,我知道Java为此提供了LinkedHashMap,但我正在尝试使用基本数据结构来实现它。

通过阅读本主题,我了解到我需要一个HashMap,用于O(1)查找密钥和链表,以管理“最近最少使用的”驱逐策略。我发现这些引用都使用标准库hashmap但实现了自己的链表:

哈希表应该直接存储链接列表节点,如下所示。我的缓存应该存储Integer键和String值。

enter image description here

但是,在Java中,LinkedList集合不公开其内部节点,因此我无法将它们存储在HashMap中。我可以将HashMap存储索引放入LinkedList中,但是到达一个项目需要O(N)时间。所以我试着存储一个ListIterator。

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.ListIterator;

public class LRUCache {

    private static final int DEFAULT_MAX_CAPACITY = 10;

    protected Map<Integer, ListIterator> _map = new HashMap<Integer, ListIterator>();
    protected LinkedList<String> _list = new LinkedList<String>();

    protected int _size = 0;
    protected int _maxCapacity = 0;

    public LRUCache(int maxCapacity) {
        _maxCapacity = maxCapacity;
    }

    // Put the key, value pair into the LRU cache.
    // The value is placed at the head of the linked list.
    public void put(int key, String value) {

        // Check to see if the key is already in the cache.
        ListIterator iter = _map.get(key);

        if (iter != null) {
            // Key already exists, so remove it from the list.
            iter.remove(); // Problem 1: ConcurrentModificationException!
        }

        // Add the new value to the front of the list.
        _list.addFirst(value);
        _map.put(key, _list.listIterator(0));

        _size++;

        // Check if we have exceeded the capacity.
        if (_size > _maxCapacity) {
            // Remove the least recently used item from the tail of the list.
            _list.removeLast();
        }
    }

    // Get the value associated with the key.
    // Move value to the head of the linked list.
    public String get(int key) {

        String result = null;
        ListIterator iter = _map.get(key);

        if (iter != null) {

            //result = iter
            // Problem 2: HOW DO I GET THE STRING FROM THE ITERATOR?

        }

        return result;
    }

    public static void main(String argv[]) throws Exception {
        LRUCache lruCache = new LRUCache(10);

        lruCache.put(10, "This");
        lruCache.put(20, "is");
        lruCache.put(30, "a");
        lruCache.put(40, "test");
        lruCache.put(30, "some"); // Causes ConcurrentModificationException
    }
}

因此,这会导致三个问题:

问题1 :当我使用存储在HashMap中的迭代器更新LinkedList时,我收到了ConcurrentModificationException。

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:953)
    at java.util.LinkedList$ListItr.remove(LinkedList.java:919)
    at LRUCache.put(LRUCache.java:31)
    at LRUCache.main(LRUCache.java:71)

问题2 。如何检索ListIterator指向的值?我似乎只能检索next()值。

问题3 。有没有办法使用Java集合LinkedList实现这个LRU缓存,还是我真的必须实现自己的链表?

3 个答案:

答案 0 :(得分:2)

1)这不是迭代器的用途。

通过合同,如果你修改列表而不使用迭代器 - 就像你在这里一样

_list.addFirst(value);

然后该列表上的所有OPEN ITERATORS都会抛出ConcurrentModificationException。他们对不再存在的列表版本持开放态度。

2)LinkedList确实不是节点的链接列表。它是一个java.util.List,其后备实现是一个双向链接的节点列表。该List合约是它不公开对支持实现的引用的原因 - 因此像&#34;这样的操作将该节点作为节点移除,并将其移动到头部&#34;不好。这种封装是为了你自己的保护(与并发mod异常相同) - 它允许你的代码依赖于LinkedList的List语义(例如,迭代),而不用担心一些两个立方体的小丑正在黑客攻击它的内脏并打破了合同。

3)这里你真正需要的不是LinkedList。你需要的是一个Stack,它允许你将任意条目移动到头部并转移尾部。您暗示您希望快速搜索任意条目以及快速删除和快速添加,并且您希望能够随时找到尾部,以防您需要删除它。

快速寻找时间==哈希某事

快速添加/删除任意元素==链接 Something

最终元素的快速寻址== Somekinda 列表

4)您需要构建自己的链接结构...或使用LinkedHashMap。

PS LinkedHashSet作弊,它是使用LinkedHashMap实现的。

答案 1 :(得分:1)

我首先处理问题3:

正如您在问题中指出的那样,LinkedList(与所有精心设计的通用集合一样)隐藏了实现的细节,例如包含链接的节点。在您的情况下,您需要哈希映射直接引用这些链接作为地图的值。否则(例如,具有通过第三类的间接)将破坏LRU高速缓存的目的,以允许非常低的值访问开销。但是标准Java集合无法实现这一点 - 他们不会(并且不应该)提供对内部结构的直接访问。

因此,逻辑上的结论是,是的,您需要实现自己的方式来存储缓存中的项目的使用顺序。这不一定是双链表。传统上这些用于LRU高速缓存,因为最常见的操作是在访问节点时将节点移动到列表的顶部。这是一个双链表中非常便宜的操作,只需要重新链接四个节点,无需分配内存或免费。

问题1&amp; 2:

这里的根本原因是你试图将迭代器用作游标。它们被设计为被创建,逐步执行某些操作然后被处理掉。即使你克服了你所遇到的问题,我预计它们背后还会有进一步的问题。你把一个方形的钉子钉在一个圆孔里。

所以我的结论是你需要实现自己的方法来保持一个跟踪访问顺序的类中的值。然而,它可以非常简单:只需要​​三个操作:创建,获取值并从尾部删除。 create和get值都必须将节点移动到列表的头部。不从列表中间插入或删除。没有删除头部。没有搜索。老实说死得很简单。

希望这会让你开始: - )

public class <K,V> LRU_Map implements Map<K,V> {
    private class Node {
        private final V value;
        private Node previous = null;
        private Node next = null;

        public Node(V value) {
            this.value = value;
            touch();
            if (tail == null)
                tail = this;
        }

        public V getValue() {
            touch();
            return value;
        }

        private void touch() {
            if (head != this) {
                unlink();
                moveToHead();
            }
        }

        private void unlink() {
            if (tail == this)
                tail = prev;
            if (prev != null)
                prev.next = next;
            if (next != null)
                next.prev = prev;
        }

        private void moveToHead() {
            prev = null;
            next = head;
            head = this;
        }

        public void remove() {
            assert this == tail;
            assert this != head;
            assert next == null;
            if (prev != null)
                prev.next = null;
            tail = prev;
        }
    }

    private final Map<K,Node> map = new HashMap<>();
    private Node head = null;
    private Node tail = null;

    public void put(K key, V value) {
        if (map.size() >= MAX_SIZE) {
            assert tail != null;
            tail.remove();
        }
        map.put(key, new Node(value));
    }

    public V get(K key) {
        if (map.containsKey(key))
            return map.get(key).getValue();
        else
            return null;
    }

    // and so on for other Map methods
}

答案 2 :(得分:0)

另一种为这只猫设置外观的方法是实现一个扩展LinkedList的非常简单的类,但是对“同步”块内的列表运行任何修改(例如添加,删除等)。你需要每次都通过get()运行你的HashMap伪指针,但它应该可以正常工作。 e.g。

public static class StringExtensions
{
    public static string FirstCharacterToLower(this string str)
    {
        if (String.IsNullOrEmpty(str) || Char.IsLower(str, 0))
        {
            return str;
        }

        return Char.ToLowerInvariant(str[0]) + str.Substring(1);
    }
}

如果你有Eclipse或IntelliJ IDEA,那么你应该能够几乎立即自动生成你需要的方法存根,并且你可以评估哪些需要被锁定。