1.previously
周五在给使用Jfinal的项目中加上了数据库的缓存,Jfinal现在进步了呀!虽然文档不多,好歹也是有了,看了看文档中关于cache的介绍,发现也不难,所以就打算自己实现一个简单键值对的缓存,不用那个现成的ehcache了。
2.currently
自制的缓存只有实现ICache接口即可,代码略过,现在键值对映射是这样的
ConcurrentHashMap<String, ConcurrentHashMap<Object, Object>>
外层映射中的key是cacheName,内层的具体的值名和值。 写完之后发现一个问题,如果没有定时清理缓存的功能,那么之后缓存无限膨胀的话,不是出大事了么?,不行还要有一个定时缓存的功能,同时也要有个时间戳来来作为清除功能的依据,所有缓存映射变成这样
ConcurrentHashMap<Tuple2<String, LocalDateTime>, ConcurrentHashMap<Object, Object>>
使用一个LocalDateTime对象作为创建的缓存时的记录,之后每隔一定时间就清理一次过期的缓存,那么清理的代码如下
Runnable r = (() -> {
while (true) {
try {
Thread.sleep(Duration.ofDays(3).toMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("run");
this.cache.keySet().forEach(t -> {
if (LocalDateTime.now().minusDays(3).isAfter(t.f1())) {
removeAll(t.f0());
}
});
}
});
new Thread(r).start();
}
这里使用java8中的stream类型来遍历映射中的过期的缓存,好既然处理逻辑写完了。那么就在这个类启动的时候启动一个线程后台执行这个功能,这个线程每次清理完sleep一段间隔,再执行下次的清理工作,这个功能貌似完成了,so easy!
且慢上面中的runnable有一下几个问题,
- while(true)首先就会引发某种异常的问题,就是进程停止的时候,线程无法自行停止虽然,在上面的这个例子中,问题好像是不大,不过并不是一个想要变优秀的程序员做的事,另外这个while(true), 没有退出声明,这个代码有很大的问题呀,
- 线程每次睡眠的时候都要显示的处理可能出现的异常,这个看起来实在无法跟美观挂钩,
终于引题了,这种方式是比较古老的办法,现在java8在出现好久了,找找看有没有更好的方式,这一找发现一个看起来很好用的类
Executors.newScheduledThreadPool
不对,记得阿里的规范里不能使用Executors,这个大佬写的非常好了。 再找,发现一个
ScheduledThreadPoolExecutor
嗯这个类看起来不错,看一下他的方法声明
/**
* Creates a new {@code ScheduledThreadPoolExecutor} with the
* given initial parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param threadFactory the factory to use when the executor
* creates a new thread
* @throws IllegalArgumentException if {@code corePoolSize < 0}
* @throws NullPointerException if {@code threadFactory} is null
*/
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
第一个是pool的size,第二个是创建线程的工厂方法,好说,看看创建完应该使用的方法
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
我自己选的固定频率的,其实还有一个方法,是固定间隔的
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay));
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}
注意看方法中的第三个参数的名字,一个是period,一个是delay, 最后的使用ScheduledThreadPoolExecutor的代码如下
final Runnable command = (() -> {
log.info("Skr the clean thread to the custom cache at {}", LocalDateTime.now());
this.cache.keySet().forEach(t -> {
if (LocalDateTime.now().minusDays(this.CLEAN_QUEUE_PERIOD).isBefore(t.f1())) {
removeAll(t.f0());
}
// 这个方法不能使用parallel 执行,因为removeAll方法有锁,没什么意义
// 每次删除的数量也不太大,避免卡着太长时间
if (this.cache.size() > this.maxSizeForCacheQueue) {
this.cache.keySet().stream()
.sorted((x, y) -> y.f1().compareTo(x.f1()))
.limit(this.maxSizeForCacheQueue >> 3)
.forEach(e -> removeAll(e.f0()));
}
});
});
new ScheduledThreadPoolExecutor(1, r -> {
final Thread t = new Thread(r);
t.setDaemon(true);
return t;
}).scheduleAtFixedRate(command, 3, 5, TimeUnit.DAYS);
3.finally
对比最开始的代码
- while已经被schedue代替了
- 难看的try-catch也已经不见了
- 这里ThreadFactory方法中的设置每个线程都是守护线程,这个也是一个优点