用HashMap和双链表实现多线程下的LRU缓存算法(java版)

来源:互联网 发布:c语言考研题库 编辑:程序博客网 时间:2024/06/04 19:36

缓存的目的:缓存主要为了保存数据的,在项目中,开启服务器的时候,将大量被访问的数据从数据库中查到,放入到缓存中,服务器开启后,用户从前端向后台发送请求,直接从缓存中去取,不用查数据库,加快数据的访问。

我的缓存的需求:主要想保存ArticleBean(中有很多属性),加入到缓存的时候按照点击量的降序,定时更新缓存的时候能将按照点击量的降序加入到合适的位置,而查找文章的时候需要根据文章的id直接缓存中去获取,不需要遍历。

思路:
链表:插入到合适的位置,不需要数据的移动
HashMap:直接根据文章的id获取ArticleBean对象。
假设:
如果将数据分别保存到链表和HashMap中,那么数据保存了两份,占用了两份内存,在内存宝贵的情况下这样是不行的。
所以:
将ArticleBean转化为一个节点Entry,在节点中保存ArticleBean作为value、前一个节点,后一个节点,key作为键值。
好处:
1.保存在HashMap可以直接定位节点的位置
2.用节点连接起来又可以直接插入节点
3.每个节点在缓存中只有这一份

多线程:
用读写锁来实现多线程下的安全,当增加、移动、删除的时候用写锁,而查阅的时候用读锁。

我的代码:

package com.zhangyike.lru;import java.util.HashMap;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class MyLRU<K,V> {    private int cacheSize;//缓存的长度    private HashMap<K, Entry<K,V>> nodes;//缓存容器,node的建其实就是Entry的键    private int currentSize;//缓存中的元素长度    private Entry<K,V> head;//链表的头部    private Entry<K,V> tail;//链表的尾部    /*     * 该类对象,保证该类对象在服务器上只有一份。     */    private static MyLRU instance;    private static ReadWriteLock rwl = new ReentrantReadWriteLock();//用于修改缓存中数据的读写锁    private static ReadWriteLock rwlInstance = new ReentrantReadWriteLock();//用于创建对象的读写锁    /*     * 虽然getLruThread(..)方法中加了rwl.writeLock().lock();,为了以防万一,该方法中也要加锁。     * 这样不会造成死锁,属于可重入锁,这两个锁的对象是相同的,就不会重新申请锁了。     */    private MyLRU(int cacheSize){        rwlInstance.writeLock().lock();        try{            this.cacheSize = cacheSize;            currentSize = 0;            nodes = new HashMap<K,Entry<K,V>>(cacheSize);//根据提供的参数初始化容器初始化容器        }finally{            rwlInstance.writeLock().unlock();        }    }    public static MyLRU getLruThread(){        return getLruThread(16);    }    public static MyLRU getLruThread(int cacheSize){        //多个线程可以同时去读这个对象,        rwl.readLock().lock();;        try{            /*             * 如果这个对象是空的,那么就关闭这个read锁,打开写锁,去获取这个对象             */            if(instance == null){                rwl.readLock().unlock();//关闭读锁                rwlInstance.writeLock().lock();;//打开写锁                /*                 * 再次判断instance对象是否为空                 * 在多线程情况下,当多个线程同时读数据,发现instance是null,则read锁关闭,写锁打开,只有一个线程进来了,                 * 获取到对象后,写锁关闭,等待在外面的线程就依次获取到这个写锁,进来,如果不做判断,还会重新获取数据,这样显然没有必要。                 * 所以必须要加这个判断。                 */                if(instance == null){                    //获取instance对象                    instance = new MyLRU(cacheSize);                }                /*                 * 这两行的顺序无所谓,因为写锁中可以去读数据,写锁锁住的情况下说明只有当前线程持有该锁,                 * 其他线程不能去读,也不能去写,读数据不会引起并发,所以可以写中读。                 *                  * 但是读锁中不能打开写锁,如果读锁中有写锁,那么写锁会修改数据,而此时多个线程在同时读数据,那么就会出现并发问题                 */                rwl.readLock().lock();//打开读锁                rwlInstance.writeLock().unlock();;//关闭写锁                return instance;            }        }finally{            rwl.readLock().unlock();//关闭读锁        }        return instance;    }    public Entry<K, V> get(K key){        rwl.readLock().lock();        try{            Entry<K,V> node = nodes.get(key);            /*             * 为什么得到了这个节点,就把这个节点移动到链表的头部呢?             */            if (node != null) {                rwl.readLock().unlock();                rwl.writeLock().lock();                //打开写锁的目的就是将获取到的节点移动到链表的头部,如果节点就在链表的头部,那么就没必要移动了。                if (node != head) {                    moveToHead(node);                }                rwl.writeLock().unlock();                rwl.readLock().lock();                return node;            }else{                return null;            }        }finally{            rwl.readLock().unlock();        }    }    //将内容添加到节点的最头部    public void put(K key, V value){        rwl.writeLock().lock();        try{            Entry<K, V> nd =  nodes.get(key);            if (nd == null) {                //判断缓存容器的大小                //容器空的时候                if (currentSize == 0) {                    nd = new Entry<K,V>(tail,null,key,value);                    head = nd;//让该节点成为头节点                    tail = nd;//让该节点成为尾节点                    currentSize++;                }else if (cacheSize == currentSize) {//容器满的时候                    //因为尾节点的点击量最小,所以要将尾节点从链表中移除                    nodes.remove(tail.key);//HashMap中移除链表的尾节点                    removeLast();//移除最后一个                    nd = new Entry<K,V>(tail,null,key,value);                }else{                    //实际长度加1                    currentSize++;                    nd = new Entry<K,V>(tail,null,key,value);                }            }else{                nd.value = value;//覆盖节点中的值            }            //将节点移动到缓存链的最前面            moveToHead(nd);            //将节点加入到缓存中            nodes.put(key, nd);        }finally{            rwl.writeLock().unlock();        }    }    //根据key值删除数据,该数据只在链满的时候才删除。    //删除链表中的数据,只用将链表前后两个节点连接就好    public void remove(K key){        rwl.writeLock().lock();        try{            Entry<K, V> node = nodes.get(key);            if (node != null) {                if (node == head) {                    head.next.pre = null;                    head = node.next;                }                if (node == tail) {                    tail.pre.next = null;                    tail = tail.pre;                }                if (node.pre != null) {                    node.pre.next = node.next;                }                if (node.next != null) {                    node.next.pre = node.pre;                }                node = null;            }            nodes.remove(key);//删除hashtable中的链        }finally{            rwl.writeLock().unlock();        }    }    //移除双向链表的尾节点    private void removeLast() {        rwl.writeLock().lock();        try{            if (tail != null) {                //判断链表是不是只有一个节点                if (tail.pre == null) {                    head = null;                }else{                    tail.pre.next = null;                }                tail = tail.pre;            }        }finally{            rwl.writeLock().unlock();        }    }    //输出双链中的内容    public void sop(){        rwl.readLock().lock();        try{            for (Entry<K, V> node = head; node != null; node = node.next) {                System.out.println("[" + node.key + " = " + node.value + "]");            }        }finally{            rwl.readLock().unlock();        }    }    //将节点移动到最前面    private void moveToHead(Entry<K, V> node) {        rwl.writeLock().lock();        try{            //node节点就是头结点            if(node == head){                return;            }            //node节点是最后一个节点            if (node == tail) {                //让node的前一个节点的next指向null,并让前一个节点变为tail。                node.pre.next = null;                tail = node.pre;            }            //node节点前面有元素            if (node.pre != null) {                //更改node的前一个节点的next的指向                node.pre.next = node.next;            }            //node前面有元素            if (node.next != null) {                //更改node的下一个节点的pre的指向                node.next.pre = node.pre;            }            //将node节点变为头结点            if (null != head) {                node.next = head;                head.pre = node;            }            node.pre = null;            head = node;            //只有一个节点            if (tail == null) {                tail = node;            }        }finally{            rwl.writeLock().unlock();        }    }    public int size(){        rwl.readLock().lock();        try{            return currentSize;             }finally{            rwl.readLock().unlock();        }    }    public void clear(){        rwl.writeLock().lock();        try{            if (head != tail) {                Entry<K, V> node = head;                while (node != null) {                    node.value = null;                    node.pre = null;                    node = node.next;                }                currentSize = 0;            }        }finally{            rwl.writeLock().unlock();        }    }    public class Entry<K,V>{        Entry<K, V> pre;        Entry<K,V> next;        K key;        V value;        Entry(Entry<K, V> p,Entry<K,V> next,K k,V value){            this.pre = p;            this.next = next;            this.key = k;            this.value = value;        }    }}

Demo测试:

package com.zhangyike.lru;public class LRUDemo {    public static void main(String[] args) {        MyLRU<Integer, String> lru = MyLRU.getLruThread(10);        //添加100个数据,因为缓存空间是10,所以只要最后10个        for (int i = 0; i < 100; i++) {            lru.put(i,  i + "--" + i);        }        System.out.println("第一次添加缓存的结果为:");        lru.sop();        lru.put(91, 91+"**");//将91-91用91**替代,并且移动到最前方        lru.remove(93);//将93移走        System.out.println();        System.out.println("更改后缓存的结果为:");        lru.sop();    }}

测试结果:
第一次添加缓存的结果为:
[99 = 99–99]
[98 = 98–98]
[97 = 97–97]
[96 = 96–96]
[95 = 95–95]
[94 = 94–94]
[93 = 93–93]
[92 = 92–92]
[91 = 91–91]
[90 = 90–90]

更改后缓存的结果为:
[91 = 91**]
[99 = 99–99]
[98 = 98–98]
[97 = 97–97]
[96 = 96–96]
[95 = 95–95]
[94 = 94–94]
[92 = 92–92]
[90 = 90–90]

0 0
原创粉丝点击