1.第一版:最简单的缓存,用HashMap实现
先检测HashMap里面 有没有 保存过之前的计算结果,若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中【 该方法是若没有在map中找到,便模拟 从数据库中找,或 查 Es, 这里不关心具体的计算逻辑,只是进行休眠】
package imooccache;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* 最简单的缓存形式 : HashMap
* 先检测HashMap里面 有没有 保存过之前的计算结果,若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中
*/
public class ImoocCache1 {
private final HashMap<String,Integer> cash =new HashMap<>();
public Integer ccomputer(String userId) throws InterruptedException {
Integer result =cash.get(userId);
//先检测HashMap里面 有没有 保存过之前的计算结果
if (result==null){
//若缓存中找不到,那么需要计算一下结果,并保存到HashMap 中
result=doCompute(userId);
cash.put(userId,result);
}
return result;
}
/**
* 该方法是若没有在map中找到,便模拟 从数据库中找,或 查 Es,
* 这里不关心具体的计算逻辑,只是进行休眠
* @param userId
* @return
* @throws InterruptedException
*/
private Integer doCompute(String userId) throws InterruptedException{
//进行休眠
TimeUnit.SECONDS.sleep(5);
return new Integer(userId);
}
public static void main(String[] args) throws InterruptedException {
ImoocCache1 cache1=new ImoocCache1();
System.out.println("开始计算了");
Integer result =cache1.ccomputer("13");
System.out.println("第一次计算结果:"+result);
result =cache1.ccomputer("13");
System.out.println("第二次计算结果:"+result);
}
}
2.暴露出来的性能和复用性问题:
在多线程的情况下是并发不安全的,jdk1.8的HashMap在多线程的情况下也会出现死循环的问题,但是1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题。
1.可以使用:synchronized
这样就解决了多线程安全问题,但是随之带来两个不能忽视的问题:
1.性能差: 用了synchronized 之后,多个线程就不能同时访问了,使性能急剧下降,但是使用缓存就是看中性能优势
2.代码复用性差
体现:
doCompute是业务方法【可能会查数据库】,缓存在业务的方法类中写了一个hashmap, 这很不合适,缓存的内容不应该侵入到业务代码中,应该做到业务的分离,
其次在实际开发中会有多个service: 有的是服务于订单的,有的是服务于用户的,若都想用缓存,在每一个service中都创建缓存代码,侵入性极强。
若缓存逻辑有变化,在每一个创建过缓存的类都进行修改,也不科学
2.给hashmap 进行 final 修饰:
给hashmap 添加上 final :让他的引用不改变,【map的内容还是可以改变的】
3.用装饰者模式解耦:
使用装饰者模式进行代码重构,仅仅关系解耦,先不关心性能
装饰者模式参考:https://www.cnblogs.com/mingmingcome/p/9798248.html
本版本使用的是简化版的装饰者模式:即丢弃抽象装饰类,让具体装饰类 实现 抽象构件角色
1. 抽象构件角色:
* 有一个计算函数 computer , 用来代表耗时计算 ,每一个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
/**
* 有一个计算函数 computer , 用来代表耗时计算 ,每一个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
*/
public interface Computable <A,V>{
V compute(A arg) throws Exception ;
}
2.具体构件角色:
耗时计算的实现类,实现了 Computable接口,但是本身 不具备缓存功能, * 也不需要考虑缓存的事情
/**
* 耗时计算的实现类,实现了 Computable接口,但是本身 不具备缓存功能,
* 也不需要考虑缓存的事情
*/
public class ExpensiveFunction implements Computable<String ,Integer> {
@Override
public Integer compute(String arg) throws Exception {
Thread.sleep(5000);
return Integer.valueOf(arg);
}
}
3.具体装饰类
/**
* 用装饰者模式,给计算机自动添加缓存功能
*/
public class ImoocCache2<A,V> implements Computable<A,V> {
private final Map<A,V> cache =new HashMap<>();
private final Computable<A,V> c;
public ImoocCache2(Computable<A,V> c){
this.c=c;
}
@Override
public synchronized V compute(A arg) throws Exception {
System.out.println("开始进行缓存机制");
V result =cache.get(arg);
if (result==null){
result=c.compute(arg);
cache.put(arg,result);
}
return result;
}
public static void main(String[] args) throws Exception {
ImoocCache2<String,Integer> expensiveComputer =new ImoocCache2<>(new ExpensiveFunction());
Integer result =expensiveComputer.compute("666");
System.out.println("第一次计算结果:"+result);
result =expensiveComputer.compute("666");
System.out.println("第二次计算结果:"+result);
}
}
1. 以后使用缓存只需要两行代码:
ImoocCache2<String,Integer> expensiveComputer =new ImoocCache2<>(new ExpensiveFunction()); Integer result =expensiveComputer.compute("666");
对于不同的功能,只需改变 具体构件类即可,将其放入 缓存类的构造函数中 :
ImoocCache2<String,Integer> expensiveComputer =new ImoocCache2<>(new ExpensiveFunction());
2. 若要修改缓存逻辑,只需要修改缓存类即可,不需要在每一个service类中进行修改
4.用ConcurrentHashMap保证并发安全:
上一版本的使用装饰者进行解耦,但是性能很差,【因为使用synchronized,只能一个一个的来,不适用与多线程】
1. 优化锁性能:减小锁的粒度
将 synchronized 从锁住方法,转到 锁住put操作 ,这样提高了并发效率,但 这并不意味着线程安全,因为HashMap还要考虑到同时读写情况
2. 使用ConcurrentHashMap 优化缓存:
使用 ConcurrentHashMap 即可,不需要加锁了
package imooccache;
import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ImoocCache3<A,V> implements Computable<A,V> {
private final Map<A,V> cache =new ConcurrentHashMap<>();
private final Computable<A,V> c;
public ImoocCache3(Computable<A,V> c){
this.c=c;
}
@Override
public V compute(A arg) throws Exception {
System.out.println("开始进行缓存机制");
V result =cache.get(arg);
if (result==null){
result=c.compute(arg);
cache.put(arg,result);
}
return result;
}
public static void main(String[] args) throws Exception {
ImoocCache2<String,Integer> expensiveComputer =new ImoocCache2<>(new ExpensiveFunction());
Integer result =expensiveComputer.compute("666");
System.out.println("第一次计算结果:"+result);
result =expensiveComputer.compute("666");
System.out.println("第二次计算结果:"+result);
}
}
5.用Future解决重复计算问题:
package imooccache;
/**
* 利用Future解决重复计算问题
*/
import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ImoocCache4<A,V> implements Computable<A,V> {
private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
private final Computable<A,V> c;
public ImoocCache4(Computable<A,V> c){
this.c=c;
}
@Override
public V compute(A arg) throws Exception {
System.out.println("开始进行缓存机制");
Future<V> f=cache.get(arg);
//重复计算都是因为没有从cache找到
if (f==null){
//计算任务
Callable<V> callable=new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> futureTask=new FutureTask<V>(callable);
//注意:此时 futureTask 还没有开始计算,只是将null赋值给f
f=futureTask;
//cache.put 放在 futureTask.run()执行之前的好处:由于ConcurrentHashMap的可见性
//保障了,一旦cache有值,其余线程就能立刻看见有值,拿到的f不为空,就不会进入 if (f==null) ,而是调用f.get()
//但是在未计算完成前,会阻塞在f.get(),直到成功返回值,此时第一个线程执行 futureTask.run(),执行完后,futureTask就有值了;
//多个线程都会从 f.get()拿到值
cache.put(arg,futureTask);
futureTask.run();
System.out.println("从FutureTask调用了计算函数");
}
return f.get();
}
public static void main(String[] args) throws Exception {
ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
new ExpensiveFunction());
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第一次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第三次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("667");
System.out.println("第二次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
6.原子组合操作填补漏洞:
使用了 ConcurrentHashMap.putIfAbsent()
package imooccache;
/**
* 利用Future解决重复计算问题
*/
import imooccache.computable.Computable;
import imooccache.computable.ExpensiveFunction;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ImoocCache4<A,V> implements Computable<A,V> {
private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
private final Computable<A,V> c;
public ImoocCache4(Computable<A,V> c){
this.c=c;
}
@Override
public V compute(A arg) throws Exception {
System.out.println("开始进行缓存机制");
Future<V> f=cache.get(arg);
//重复计算都是因为没有从cache找到
if (f==null){
//计算任务
Callable<V> callable=new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> futureTask=new FutureTask<V>(callable);
//注意:此时 futureTask 还没有开始计算,只是将null赋值给f
f=futureTask;
//putIfAbsent 返回的是本次操作之前的value【对于第一个线程来说:f=null】
// 对于第二个线程来说,发现map中已经有值了,并且 接收上一次操作放入的值futureTask
f=cache.putIfAbsent(arg,futureTask);
if (f==null){
f=futureTask;
futureTask.run();
System.out.println("从FutureTask调用了计算函数");
}
}
return f.get();
}
public static void main(String[] args) throws Exception {
ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
new ExpensiveFunction());
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第一次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666");
System.out.println("第三次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("667");
System.out.println("第二次的计算结果:" + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
7.计算错误如何处理?:
+
模拟异常:
有大约50%的机率出错
/**
* 描述: 耗时计算的实现类,有概率计算失败
*/
public class ComputeMayThrowException implements Compute<String, Integer>{
@Override
public Integer compute(String arg) throws Exception {
// 模拟实际业务的计算耗时
double random = Math.random();
if (random > 0.5) {
throw new IOException("读取文件错误: " + random);
}
Thread.sleep(3000);
return arg.hashCode();
}
}
2.对于缓存类 捕获异常,对于各种异常进行不同逻辑的处理的
package imooccache;
import imooccache.computable.Computable;
import imooccache.computable.MayFail;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.*;
/**
* 出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效
* ,那么会带来缓存不一致等问题
*/
public class ImoocCache5<A,V> implements Computable<A,V> {
private final Map<A, Future<V>> cache =new ConcurrentHashMap<>();
private final Computable<A,V> c;
public ImoocCache5(Computable<A,V> c){
this.c=c;
}
@Override
public V compute(A arg) throws InterruptedException, ExecutionException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<>(callable);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
System.out.println("从FutureTask调用了计算函数");
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
System.out.println("被取消了");
cache.remove(arg);
throw e;
} catch (InterruptedException e) {
cache.remove(arg);
throw e;
} catch (ExecutionException e) {
System.out.println("计算错误,需要重试");
cache.remove(arg);
}
}
}
}
注意:此时还是有几率发生重复计算:
当计算失败后,重新计算时,需要remove操作:
当前一个线程putIfAbsent放入值后,计算失败,此时remove,然后又有一个线程去putIfAbsent时会发现拿到的future是null,所以又会计算一次。
改进:
在捕获ExecutionException 的异常中使用了 replace方法,重新构建一个计算的FutureTask并尝试替换,理论上并发下只会有一个替换成功,其他线程替换失败则会跳过此步骤。代码如下
import java.util.Objects;
import java.util.concurrent.*;
/**
* @auth Hahadasheng
* @since 2020/11/27
*/
public class ComputeHandler implements Compute<String, Integer> {
/**
* 使用Future的目的是在并发访问下,如果请求的计算的值正在计算中,那么
* 对应的请求线程会阻塞直到计算完毕
*/
private static final ConcurrentHashMap<String, Future<Integer>>
cache = new ConcurrentHashMap<>();
/**
* 使用装饰器模式进行解耦
*/
//private Compute<String, Integer> compute = new DefaultCompute();
private Compute<String, Integer> compute = new ComputeMayThrowException();
@Override
public Integer compute(String in) throws Exception {
while (true) {
Future<Integer> future = cache.get(in);
if (Objects.isNull(future)) {
FutureTask<Integer> futureTask = new FutureTask<>(
() -> compute.compute(in));
/* 这里实现避免并发情况下的重复计算问题!
* putIfAbsent是一个线程安全,利用CAS原理的设置方法,
* 当且仅当设置的key在Map中不存在才会添加进去,并且返回null
* 否则设置失败并返回Map中已有的对象
* */
future = cache.putIfAbsent(in, futureTask);
if (Objects.isNull(future)) {
// 说明此时设置成功
future = futureTask;
System.out.println(Thread.currentThread().getName() + "从FutureTask中调用了计算函数");
futureTask.run(); /* 注意:这里没有启动新的线程,只是单纯想用run方法而已 */
}
}
/*
如果计算过程中出现异常又如何处理
如果Future在计算过程中被cancel取消,则会抛出CancellationException异常
*/
try {
return future.get();
} catch (CancellationException e) {
// future.cancel() 取消的情况
System.out.println(Thread.currentThread().getName() + "计算结果被取消");
cache.remove(in);
throw e;
} catch (InterruptedException e) {
// thread.interrupt() 中断线程
System.out.println(Thread.currentThread().getName() + "计算线程被中断");
cache.remove(in);
throw e;
} catch (ExecutionException e) {
System.out.println(Thread.currentThread().getName() + e.getLocalizedMessage() + "::计算错误,尝试重试...");
/* 如果计算错误,需要将错误的future进行移除,
/ 直接调用cache.remove(in)也有重复计算的风险 */
FutureTask<Integer> futureTaskAgain = new FutureTask<>(
() -> compute.compute(in));
if (cache.replace(in, future, futureTaskAgain)) {
System.out.println(Thread.currentThread().getName() + "异常重试计算:从FutureTask中调用了计算函数");
futureTaskAgain.run();
}
}
}
}
}
8.缓存过期功能和随机性:
出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效 ,那么会带来缓存不一致等问题
使用ScheduledExecutorService 支持定时及周期性任务执行
public final static ScheduledExecutorService executer= Executors.newScheduledThreadPool(5);
重载了compute函数
public V compute(A arg,long expire) throws ExecutionException, InterruptedException {
if (expire>0){
executer.schedule(new Runnable() {
@Override
public void run() {
expire(arg);
}
},expire,TimeUnit.MILLISECONDS);
}
//过期缓存被清除后,再次进行查找
return compute(arg);
}
清除缓存的具体实现方法
public synchronized void expire(A key) {
Future<V> future=cache.get(key);
if (future !=null){
if (!future.isDone()){
System.out.println("Future 任务被取消");
future.cancel(true);
}
System.out.println("过期时间到,缓存被清除");
cache.remove(key);
}
}
// 缓存过期时间随机
// 缓存过期时间随机
public V computeRandomExpire(A arg) throws ExecutionException, InterruptedException {
long randomExpire = (long) (Math.random()*10000);
return compute(arg,randomExpire);
}
9.用线程池测试缓存性能:
package imooccache;
import imooccache.computable.ExpensiveFunction;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ImoocCacheTest {
static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
public static void main(String[] args) {
ExecutorService service= Executors.newFixedThreadPool(3000);
long start =System.currentTimeMillis();
for (int i=0;i<300;i++){
service.submit(new Runnable() {
@Override
public void run() {
Integer result=null;
try {
result=expensiveComputer.compute("666");
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
});
}
service.shutdown();
while (!service.isTerminated()){
}
System.out.println("总耗时:"+(System.currentTimeMillis()-start));
}
}
10.用CountDownLatch实现压测:
上边是随着任务提交到线程池就执行,这样使压力不集中,
想要看到线程统一出发,这样给服务器带来压力
public class ImoocCacheTest {
static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
public static CountDownLatch countDownLatch=new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
ExecutorService service= Executors.newFixedThreadPool(3000);
long start =System.currentTimeMillis();
for (int i=0;i<300;i++){
service.submit(new Runnable() {
@Override
public void run() {
Integer result=null;
try {
System.out.println(Thread.currentThread().getName()+"开始等待");
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"被放行");
result=expensiveComputer.compute("666");
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
});
}
Thread.sleep(5000);
countDownLatch.countDown();
service.shutdown();
}
}
11.用ThreadLocal确认时间的统一性
使用ThreadLocal ,类型为SimpleDateFormat("mm:ss")主要是查看每一个线程被放行的时间【分:秒】
若时间集中在一两秒内,则测压成功
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
package imooccache;
import imooccache.computable.ExpensiveFunction;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ImoocCacheTest {
static ImoocCache5<String,Integer>expensiveComputer=new ImoocCache5<>(new ExpensiveFunction());
public static CountDownLatch countDownLatch=new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
ExecutorService service= Executors.newFixedThreadPool(3000);
long start =System.currentTimeMillis();
for (int i=0;i<300;i++){
service.submit(new Runnable() {
@Override
public void run() {
Integer result=null;
try {
System.out.println(Thread.currentThread().getName()+"开始等待");
countDownLatch.await();
SimpleDateFormat dateFormat =ThreadSafeFormatter.dateFormatThreadLocal.get();
String time =dateFormat.format(new Date());
System.out.println(Thread.currentThread().getName()+" "+ time+"被放行");
result=expensiveComputer.compute("666");
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
});
}
Thread.sleep(5000);
countDownLatch.countDown();
service.shutdown();
}
}
class ThreadSafeFormatter{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
};
}
12.总结:
1.首先实现了一个简单的HashMap缓存,发现有很多缺陷
1.线程不安全:
改进1: 使用了synchronized,保证了线程安全,但性能急剧下降【后续解决】
2.
代码复用性差:缓存代码和缓存计算业务代码混在一起,侵入性强
改进:使用了装饰者模式:进行解耦
3.性能初优化:减小锁的粒度:只在map.put()上加锁,提高了并发效率,但 这并不意味着线程安全,因为HashMap还要考虑到同时读写情况
性能进一步优化:使用ConcurrentHashMap,不用人为加锁
4.发现在多线程情况下:同一值可能会计算两次,使用Future 和Callable避免了重复计算
仅仅解决了大部分的重复计算,在两个线程同时计算同一值,同时调用map.get(),返回值都为null时,还是会
创建两个任务去计算同一值,使用了原子操作 map.putIfAbsent()解决了这一问题
5.为了解决:计算中抛出的异常ExecutionException,InterruptedException和CancellationException用
不同的catch语句进行捕获,使用不同的逻辑进行处理。对于后两个异常(一般都是人为的,所以直接抛出异常),
对于计算错误,则使用while(true)保证
计算成功【尝试多次直至成功】
6.但是出现了缓存污染问题,所以计算失败则移除Future,增强了健壮性
7.为了解决缓存过期问题,为每一个结果指定了过期时间,并使用ScheduledThreadPool 进行定期扫描过期元素
8.为了解决在高并发访问,同时过期,造成缓存雪崩,缓存击穿,
则使缓存时间随机化,让缓存更见安全
9.为了观测缓存效果,使用了线程池创建大量线程,使用countDownLatch对所有线程进行统一放行,达到小范围压测的目的,并且使用 ThreadLocal<SimpleDateFormat> 使每一个线程都打印当前时间来验证,压测的准确性