哈哈哈,终于考完试了,用了大概两天时间肝了这篇文章!!!
关于今天要讲的mybatis缓存机制,其实之前我已经有看过也用过,只不过平常不太留意,最近在看mybatis源码,就来讲一下这个缓存机制
本次分析的代码和数据表在gitee上,地址:https://gitee.com/professor_mai/mybatis_cache_demo
关于这个Mybatis缓存,推荐这篇文章 https://tech.meituan.com/2018/01/19/mybatis-cache.html,下面的内容是基于这篇文章来写的,我写的内容是更偏重于原理级别的。
再次提醒,一定要把上面推荐的文章再来看下面的内容,不然你会很懵逼
在进行数据库查询之前,MyBatis 首先会检查以及缓存中是否有相应的记录,若有的话直接返回即可。一级缓存是数据库的最后一道防护,若一级缓存未命中,查询请求将落到数据库上。一级缓存是在 BaseExecutor 被初始化的。
实验一:开启一级缓存,调用三次相同的查询操作
通过上面文章大致了解了一级缓存后(再次提醒,一定要看上面推荐的文章),可以看看查询一级缓存的逻辑。
经过上图的调用之后,最终是在BaseExecutor
的query
方法上执行。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 省略部分代码
List<E> list;
try {
queryStack++;
// 查询一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 存储过程相关逻辑,忽略
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,则从数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
// 省略部分代码
return list;
}
如上的代码,就是查询一级缓存的实质,我们再一步一步细分一下
MyBatis 首先会调用 createCacheKey 方法创建 CacheKey,我们可以简单的把 CacheKey 看做是一个查询请求的 id
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 创建 CacheKey 对象
CacheKey cacheKey = new CacheKey();
// 将 MappedStatement 的 id 作为影响因子进行计算
cacheKey.update(ms.getId());
// RowBounds 用于分页查询,下面将它的两个字段作为影响因子进行计算
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
// 获取 sql 语句,并进行计算
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value; // 运行时参数
// 当前大段代码用于获取 SQL 中的占位符 #{xxx} 对应的运行时参数,
// 前文有类似分析,这里忽略了
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
// 让运行时参数参与计算
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// 获取 Environment id 遍历,并让其参与计算
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
在上面代码中,若一级缓存为命中(很明显在我们的实验中,这个实验是参考我们上面这个文章的),BaseExecutor 会调用 queryFromDatabase 查询数据库,并将查询结果写入缓存中。下面看一下 queryFromDatabase 的逻辑。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 向缓存中存储一个占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 查询数据库
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 移除占位符
localCache.removeObject(key);
}
// 存储查询结果
localCache.putObject(key, list);
// 存储过程相关逻辑,忽略
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
那执行完之后,就已经存进这个localcache变量里面了,下次我们只需要直接getobject
拿就行了。
实验二:增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效
这里重点说一下,为什么一级缓存会失效:
缓存究竟是在哪里拿的?
上面我们说过是从localCache.getObject
中拿的,我们再往深一层,会发现调用的是PerpetualCache #getObject
方法。
我们可以再看看,插入数据后的localCache
发生了什么变化,这是插入前的
插入后,再次查询会发现localCache已经没有了,因为其最大的共享范围就是一个 SqlSession 内部,
也可以简单看一下这个调用栈,Executor是不直接暴露接口的,是通过Sqlsession接口的。
可能到这里你还是有点懵,这里再来简单说一下吧,先简单看看这个调用栈
insert语句会调用BaseExecutor#update方法,有朋友就很奇怪,为什么insert语句是调用update方法呢?为什么不是调用insert方法呢?这里简单提一下,答案是:只提供一个 update 方法从实现上完全可行,但是从接口的语义化的角度来说,这样做并不好。一般情况下,使用者觉得 update 接口方法应该仅负责执行 UPDATE 语句,如果它还兼职执行其他的 SQL 语句,会让使用者产生疑惑。对于对外的接口,接口功能越单一,语义越清晰越好。在日常开发中,我们为客户端提供接口时,也应该这样做。
接下来就来看看这个update方法做了什么操作
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 刷新一级缓存(看到方法名就知道是清空缓存了)
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
// 其实在这之前还会先调用CachingExecutor#update方法,看上面调用栈就知道了
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
// 刷新二级缓存(这里也会清除掉二级缓存)
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
后面会出一篇Mybatis的执行Sql命令的完整流程,估计看完你就不懵了,敬请期待
实验三:开启两个
SqlSession
,在sqlSession1
中查询数据,使一级缓存生效,在sqlSession2
中更新数据库,验证一级缓存只在数据库会话内部共享。
通过调试,其实这两个sqlSession
是两个不同的sqlSession
更新语句,只能清楚sqlSession2
的缓存,并不能清除sqlSession1
的缓存,所以会出现脏读,所以一级缓存只在数据库会话内部共享。
STATEMENT
级别是一种缓存级别,可以理解为缓存只对当前执行的这一个Statement
有效,可以看看BaseExecutor
#query
方法最后会用到这个判断来判断是不是STATEMENT
,如果是就会清空缓存!STATEMENT
级别之后,有没有会出现,很明显是不会出现的,同时,一级缓存也失效了! 在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor ,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
实验一:测试二级缓存效果,不提交事务,
sqlSession1
查询完数据后,sqlSession2
相同的查询是否会从缓存中获取数据。
在讲结论之前,先简单介绍一下,支持二级缓存的 Executor 的实现类
CachingExecutor
private Executor delegate;
// TransactionalCacheManager 对象,支持事务的缓存管理器。因为二级缓存是支持跨 Session 进行共享,此处需要考虑事务,那么,必然需要做到事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中。这个功能,就通过 TransactionalCacheManager 来实现。
private TransactionalCacheManager tcm = new TransactionalCacheManager();
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
//里面有个query方法,重点
// CachingExecutor.java
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获得 BoundSql 对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey 对象
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 查询
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// <1> 调用 MappedStatement#getCache() 方法,获得 Cache 对象,即当前 MappedStatement 对象的二级缓存。
Cache cache = ms.getCache();
if (cache != null) { // <2> 如果有 Cache 对象,说明该 MappedStatement 对象,有设置二级缓存
// <2.1> 如果需要清空缓存,则进行清空,注意这里清空缓存,只是清空未提交事务之前的缓存,而真正的清空,在事务的提交时。这是为什么呢?还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) { // <2.2>
// 暂时忽略,存储过程相关
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// <2.3> 从二级缓存中,获取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// <2.4.1> 如果不存在,则从数据库中查询
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// <2.4.2> 缓存结果到二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
// <2.5> 如果存在,则直接返回结果
return list;
}
}
// <3> 不使用缓存,则从数据库中查询,如果没有 Cache 对象,说明该 MappedStatement 对象,未设置二级缓存,则调用 delegate 属性的 #query方法,直接从数据库中查询
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看看
TransactionalCacheManager
,TransactionalCache
管理器
// TransactionalCacheManager.java
/** * Cache 和 TransactionalCache 的映射 */
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
这个类的主要作用如上图所示,就是要维护Cache 和TransactionalCache
的关系
TransactionalCache 是怎么创建的呢?答案在 #getTransactionalCache(Cache cache)
方法,代码如下:
// TransactionalCacheManager.java
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
transactionalCaches
获得 Cache 对象,对应的 TransactionalCache 对象。transactionalCaches
中。#commit()
方法,提交所有 TransactionalCache 。代码如下:
// TransactionalCacheManager.java
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
再来看看上面提到的
TransactionalCache
// TransactionalCache.java
/**
* 委托的 Cache 对象。
*
* 实际上,就是二级缓存 Cache 对象。
*/
private final Cache delegate;
/**
* 提交时,清空 {@link #delegate}
*
* 初始时,该值为 false
* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态
*/
private boolean clearOnCommit;
/**
* 待提交的 KV 映射,在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
*/
private final Map<Object, Object> entriesToAddOnCommit;
/**
* 查找不到的 KEY 集合, 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
*/
private final Set<Object> entriesMissedInCache;
public TransactionalCache(Cache delegate) {
this.delegate = delegate;
this.clearOnCommit = false;
this.entriesToAddOnCommit = new HashMap<>();
this.entriesMissedInCache = new HashSet<>();
}
@Override
public Object getObject(Object key) {
// <1> 从 delegate 中获取 key 对应的 value
Object object = delegate.getObject(key);
// <2> 如果不存在,则添加到 entriesMissedInCache 中,这个操作真的是神操作啊!!!!,看看下面的commit() 和 rollback() 方法
if (object == null) {
entriesMissedInCache.add(key);
}
// <3> 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 null
if (clearOnCommit) {
return null;
// <4> 返回 value
} else {
return object;
}
}
public void commit() {
// <1> 如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
// 调用 flushPendingEntries() 方法,将 entriesToAddOnCommit、entriesMissedInCache 同步到 delegate 中
flushPendingEntries();
// 重置
reset();
}
private void flushPendingEntries() {
// 将 entriesToAddOnCommit 刷入 delegate 中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
// 将 entriesMissedInCache 刷入 delegate 中
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
public void rollback() {
// <1> 从 delegate 移除出 entriesMissedInCache
unlockMissedEntries();
// <2> 重置
reset();
}
大致介绍完了上面的类,可以先说一下结论了当sqlsession
没有调用commit()
方法时,二级缓存并没有起到作用,我们先看看下面的提交了事务之后为什么可以查到缓存说起吧。
看完下面的内容,应该这里就很容易理解了,也就是未提交之前,查询会存在entriesMissedInCache
或者entriesToAddOnCommit
上,还没有刷回去delegate集合
,所以就查不到缓存了!!
其实,这里我一直有个疑惑,为什么还要这么麻烦,提交事务之后又要刷一次缓存,为什么不直接用这个entriesMissedInCache
或者entriesToAddOnCommit
上的缓存呢?
这里参考了一下别人的博客的这张图最终是明白了,这里我把这张图放出来,相信你也会明白
这里如果直接用的话,像上面那样子就会出现脏数据问题。
但需要注意的时,MyBatis 缓存事务机制只能解决脏读问题,并不能解决“不可重复读”问题。再回到上图,事务 B 在被提交前进行了三次查询。前两次查询得到的结果为记录 A,最后一次查询得到的结果为 A′。最有一次的查询结果与前两次不同,这就会导致“不可重复读”的问题。MyBatis 的缓存事务机制最高只支持“读已提交”,并不能解决“不可重复读”问题。即使数据库使用了更高的隔离级别解决了这个问题,但因 MyBatis 缓存事务机制级别较低。此时仍然会导致“不可重复读”问题的发生,这个在日常开发中需要注意一下。
-from 田小波的博客
测试二级缓存效果,当提交事务时,
sqlSession1
查询完数据后,sqlSession2
相同的查询是否会从缓存中获取数据。
sqlSession1.close();
实验代码中,有这个代码是代表关闭这个Session,然后就会自动提交事务
下面主要分析一下这个方法的调用栈
// DefaultSqlSession
@Override
public void close() {
try {
// 进去这个方法
executor.close(isCommitOrRollbackRequired(false));
closeCursors();
dirty = false;
} finally {
ErrorContext.instance().reset();
}
}
@Override
public void close(boolean forceRollback) {
try {
//issues #499, #524 and #573
// 会先判断是不是会滚操作
if (forceRollback) {
tcm.rollback();
} else {
// 提交操作
tcm.commit();
}
} finally {
delegate.close(forceRollback);
}
}
// 本质:又到了这里了,下面就不用说了,就是把那些缓存刷进去
public void commit() {
// <1> 如果 clearOnCommit 为 true ,则清空 delegate 缓存
if (clearOnCommit) {
delegate.clear();
}
// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
// 调用 flushPendingEntries() 方法,将 entriesToAddOnCommit、entriesMissedInCache 同步到 delegate 中
flushPendingEntries();
// 重置
reset();
}
然后,我们可以看是否可以查询二级缓存成功,结果肯定没问题了,通过这个TransactionalCache#getObject
方法拿到缓存
实验3:测试
update
操作是否会刷新该namespace
下的二级缓存。
先来看看调用栈:
看看这个刷新缓存的本质:
// TransactionalCache.java
@Override
public void clear() {
//方便下面清空真正的缓存
clearOnCommit = true;
// 清空事务未提交的缓存
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
// 当事务提交的时候会清空真正的缓存
delegate.clear();
}
flushPendingEntries();
reset();
}
结论很明显了:,在sqlSession3
更新数据库,并提交事务后,sqlsession2
的StudentMapper namespace
下的查询走了数据库,没有走Cache
实验四和实验五就不多说了,挺简单的
具有 LRU 策略的缓存 LruCache
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
public LruCache(Cache delegate) {
this.delegate = delegate;
setSize(1024);
}
public int getSize() {
return delegate.getSize();
}
public void setSize(final int size) {
/*
* 初始化 keyMap,注意,keyMap 的类型继承自 LinkedHashMap,
* 并覆盖了 removeEldestEntry 方法
*/
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
private static final long serialVersionUID = 4267176411845948333L;
// 覆盖 LinkedHashMap 的 removeEldestEntry 方法
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
// 获取将要被移除缓存项的键值
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public void putObject(Object key, Object value) {
// 存储缓存项
delegate.putObject(key, value);
cycleKeyList(key);
}
@Override
public Object getObject(Object key) {
// 刷新 key 在 keyMap 中的位置
keyMap.get(key);
// 从被装饰类中获取相应缓存项
return delegate.getObject(key);
}
@Override
public Object removeObject(Object key) {
// 从被装饰类中移除相应的缓存项
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
keyMap.clear();
}
private void cycleKeyList(Object key) {
// 存储 key 到 keyMap 中
keyMap.put(key, key);
if (eldestKey != null) {
// 从被装饰类中移除相应的缓存项
delegate.removeObject(eldestKey);
eldestKey = null;
}
}
// 省略部分代码
}
上面我们用到的缓存都是可保证线程安全的缓存 SynchronizedCache
,那关于这个LRU缓存就简单拓展一下JDK的LinkedHashMap
LinkedHashMap ,在 HashMap 的基础之上,提供了顺序访问的特性:
按照 key-value 的插入顺序进行访问
按照 key-value 的访问顺序进行访问
谈谈LinkedHashMap 的Entry
下面还是通过一个具体的案例来理解这个类的作用吧
基于 LinkedHashMap 实现LRU缓存
关于LRU就不多说了,可以我写过的内存管理,那里有简单提到过!
public class SimpleCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_NODE_NUM = 100;
private int limit;
public SimpleCache(){
this(MAX_NODE_NUM);
}
public SimpleCache(int limit) {
super(limit, 0.75f, true);
this.limit = limit;
}
public V save(K key, V val){
return put(key, val);
}
public V getOne(K key) {
return get(key);
}
public boolean exists(K key) {
return containsKey(key);
}
/**
* 判断节点数是否超限
* @param eldest
* @return 超限返回 true,否则返回 false
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > limit;
}
}
// 测试类
public class SimpleCacheTest {
@Test
public void test() throws Exception {
SimpleCache<Integer, Integer> cache = new SimpleCache(3);
for (int i = 0; i < 10; i++) {
cache.save(i, i * i);
}
System.out.println("插入10个键值对后,缓存内容:");
System.out.println(cache + "\n");
System.out.println("访问键值为7的节点后,缓存内容:");
cache.getOne(7);
System.out.println(cache + "\n");
System.out.println("插入键值为1的键值对后,缓存内容:");
cache.save(1, 1);
System.out.println(cache);
}
}
我们不妨可以先看看结果,到底这个LinkedHashMap有什么功能那么强大
在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。
我们可以不妨debug进去源码看看它是怎么插入和顺序是怎么调整的!!
首先是看看LinkedHashMap
的构造函数
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
// 增加了一个标志顺序的变量
this.accessOrder = accessOrder;
}
然后是这个save
方法,内部调用的还是hashmap的putVal
方法
// LinkedHashMap并没有覆写该方法,但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用过程:
在HashMap
的putVal
方法中先调用newNode
,而newNode
方法被LinkedHashMap
覆写,最终调用的是LinkedHashMap
的newNode
方法,我们来看看这个方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 这里又调用了linkNodeLast方法将 Entry 接在双向链表的尾部,实现了双向链表的建立
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
再来看看getOne
方法是怎么访问一个节点的。默认情况下,LinkedHashMap
是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap
,指定 accessOrder
参数为 true,即可让它按访问顺序维护链表。
// LinkedHashMap 覆写了get方法
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 默认情况是true
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
// 只需要将这些方法访问的节点移动到链表的尾部即可
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)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;
}
}
最后再看看这个删除过程,我们在这里调用了cache.save(1, 1);
,看看插入的过程会怎么淘汰里面的节点
可以看看调用栈:前面的过程都是一样的,唯独这里的处理有点不同,这里调用了LinkedHashMap
实现的afterNodeInsertion
方法
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// 根据条件判断是否移除最近最少被访问的节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
/**
* 自己实现的策略,判断节点数是否超限
* @param eldest
* @return 超限返回 true,否则返回 false
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > limit;
}
// 很明显这里已经超过了限制的数量,就要调用HashMap#removeNode方法
// HashMap 中实现
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode) {...}
else {
// 遍历单链表,寻找要删除的节点,并赋值给 node 变量
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) {...}
// 将要删除的节点从单链表中移除
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); // 调用删除回调方法进行后续操作
return node;
}
}
return null;
}
// LinkedHashMap 中覆写
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 将 p 节点的前驱后后继引用置空
p.before = p.after = null;
// b 为 null,表明 p 是头节点
if (b == null)
head = a;
else
b.after = a;
// a 为 null,表明 p 是尾节点
if (a == null)
tail = b;
else
a.before = b;
}
删除的过程并不复杂,上面这么多代码其实就做了三件事:
大家应该能猜到我这期个人唠叨要说什么了吧!没错,就是关于本次操作系统的期末考试,想想也很久没有这种做不完试卷的感觉了,回想起来,这种感觉也要追溯到两年前的高考了,当时做数学就是这种感觉了,做题的感觉是真的美妙,看起来都会,写上去就会出现各种各样的错误!!!好了,废话不多说了,马上进入主题!
谈谈我对这次考试的看法
相信参加了本次考试的同学都很清楚这次考试的难度是什么样的?以我来看,本次考试确实是正常难度,题量相对来说是偏多了,老师出题的目的很简单,就是想通过这份卷子来巩固我们一个学期下来学习到的知识点,所以,题量是会多了的!
其实我是很赞同他的这种想法的,有些知识点确实是你糊弄地学会了,你并不是真正地学会,而这些必修的知识点是非常重要的,一定不能随便搞!这也让我想起超哥,他当初也是这样子,为了是我们可以学得更好。
考完之后,我才发现我是真的菜,这也许是少刷题的原因吧,导致每道题有思路,但是写得都好慢,生怕会出错的样子,想不到时间也越来越紧迫了,最后导致自己的心态都崩了!嗯,确实崩了,可能是自从上大学之后太久没遇到过这种情况,导致出一点点差错,心态就容易崩。直到现在,心态才开始慢慢恢复上来!
我们应该抱着一种什么心态来看这种考试呢
回想一下,当初我高考完知道分数后,知道自己彻底完蛋了,一直持续了这种颓废的状态好久好久好久。。
回到这次考试上,如果你也像我这样,考崩了,我可以教你一个方法,那就是你要尝试把这个结果推迟一年,一年不行推迟五年,试想一下,我们挂科了,推迟一年之后,这个结果会影响你的什么?嗯,影响只不过是我要去补考,我拿不到奖学金,推迟五年,对我影响可以说是没了的,因为什么拿不到奖学金,补考这些东西对你将来的工作有影响吗?没有了,所以,我根本就不用焦虑,改过的生活还是照常过,该学习还是要继续学习
除了这个方法之外,还教大家一个方法,比如:我的目标是进大厂,那考试挂科对你进大厂有没影响呢?哈哈哈哈,这里就以我个人看法来谈谈有没影响,不喜勿喷,我觉得是根本没有影响的,你去面试的时候难道真的会认真看你简历上的成绩单吗(简历上甚至是不会放成绩单的,985 211 除外),难道真的会因为你挂过科不会给你机会吗?是不会的
谈谈笔试
我们都知道,很多大厂面试之前都是要进行笔试的,那我上面说到的那种考试的方式其实是和笔试差不多的,所以,我们不能排斥这个考试,每一次考试都要认真准备,那其实这个笔试跟我们的应试考试是有点差别的,笔试是为了筛选人才(好像这一次操作系统的考试,有筛选人才的意思,因为这个题量,你没有一定的熟练程度是没办法做完并且拿高分的),而我们的应试考试只是一个形式而已!!
谈谈我对挂科的看法
好了,这里又引申到另一个主题了,我对挂科的看法,很多人都说“没有挂过科的大学是不完整的!”,哈哈哈哈,我是完全否认这句话的,能不挂科一定不要挂科,真的,因为后续的工作是很麻烦的!当然啦,那什么是能不挂科尽量不挂科?就是你有认真在学习,有认真刷题,认真弄懂每一个知识点,但是老师就是要为难你,那你没办法,那说回这次考试,大家都觉得自己会挂科,那自己平时真的有弄懂每一个知识点吗?真的有认真刷题吗?如果你回答是,那我无话可说,挂科只能怪老天,不能怪你,但我相信绝大多数朋友(包括我在内)都不怎么会认真对待这门课,因为这门课又枯燥,又难啃,别说还另外拿时间出来刷题了!所以,挂科了也不能怨别人了,只能怪自己并没有好好重视这门课!
那挂了科真的代表你不行了吗?我觉得,考试考得好只能说明你做题真的很牛逼!但是你试想一下,你真正干活的时候,真的是像考试做题那样吗?那其实并不是这样的,我们会发现很多成绩特别特别优异的同学,他们的编程能力其实并不好(我觉得我大一的时候就是这样一种人,过分追求分数了!!)。在大学的时候,那些编程能力最强的往往是那些成绩比较一般的。
为什么会这样呢?
我觉得主要是一个思维的转变问题。很多人学习编程的时候,总是想着我要把这个 API 、把这种题型记下来,把这个库的用法记下来。这样学习,导致的结果只有一个那就是你会很难受!因为,这些根本不是要死记硬背的东西啊!真还当这是上课考试啊!**你要从如何用你学的东西来解决实际编程问题出发,站在做一个实际的项目的角度来学习。**所以,我认为做项目,写课程设计报告真的是一种很好的学习方式!
好了,总结一下,我们不要把学习编程还当做一场应试考试来看了!!!!
花了近一个小时来写这个个人唠叨,目的不为啥,就是为了想让你们能和我一样尽快建立一种对考试的正确认知!!不要让挂科覆盖了你的整个人生!最后,希望大家不要以应试考试的方式学习编程、多实践、造轮子是一种特别能够提高自己系统编程能力的手段等等。说了这么多,如果你没有将这些学习编程的正确姿势用到自己平时学习中的话,这篇文章对你的帮助可能非常有限。
接下来这个暑假,希望大家能一起加油,一起变强,共勉!!!