缓存的目的:缓存主要为了保存数据的,在项目中,开启服务器的时候,将大量被访问的数据从数据库中查到,放入到缓存中,服务器开启后,用户从前端向后台发送请求,直接从缓存中去取,不用查数据库,加快数据的访问。
我的缓存的需求:主要想保存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 {
private int cacheSize;//缓存的长度
private HashMap> nodes;//缓存容器,node的建其实就是Entry的键
private int currentSize;//缓存中的元素长度
private Entry head;//链表的头部
private Entry 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>(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 get(K key){
rwl.readLock().lock();
try{
Entry 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 nd = nodes.get(key);
if (nd == null) {
//判断缓存容器的大小
//容器空的时候
if (currentSize == 0) {
nd = new Entry(tail,null,key,value);
head = nd;//让该节点成为头节点
tail = nd;//让该节点成为尾节点
currentSize++;
}else if (cacheSize == currentSize) {//容器满的时候
//因为尾节点的点击量最小,所以要将尾节点从链表中移除
nodes.remove(tail.key);//HashMap中移除链表的尾节点
removeLast();//移除最后一个
nd = new Entry(tail,null,key,value);
}else{
//实际长度加1
currentSize++;
nd = new Entry(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 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 node = head; node != null; node = node.next) {
System.out.println("[" + node.key + " = " + node.value + "]");
}
}finally{
rwl.readLock().unlock();
}
}
//将节点移动到最前面
private void moveToHead(Entry 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 node = head;
while (node != null) {
node.value = null;
node.pre = null;
node = node.next;
}
currentSize = 0;
}
}finally{
rwl.writeLock().unlock();
}
}
public class Entry{
Entry pre;
Entry next;
K key;
V value;
Entry(Entry p,Entry 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 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]