High Performance Cache
缓存作用
缓存是在实际生产中非常常用的工具,用了缓存以后,我们可以避免重复计算,提高吞吐量。
一个功能完备、性能强劲的缓存,需要考虑的点非常多。
第一阶段
- 如果不使用
synchronized
多线程同时 put、扩容时候会出现线程安全隐患 - 使用
synchronized
性能差,多线程需要串行化的方式执行doCompute
- 代码复用性差,代码侵入性强,违反开闭原则。
1* 性能差:多个线程同时想计算的时候,需要慢慢等待(同步方法)
1import java.util.HashMap;
2import java.util.Map;
3import java.util.concurrent.TimeUnit;
4
5/**
6 * 描述: 最简单的缓存形式:HashMap
7 */
8public class ImoocCache1 {
9 //final 增加安全性,可读性
10 private final HashMap<String,Integer> cache = new HashMap<>();
11
12 public synchronized Integer computer(String userId) throws InterruptedException {
13 Integer result = cache.get(userId);
14 //先检查HashMap里面有没有保存过之前的计算结果
15 if (result == null) {
16 //如果缓存中找不到,那么需要现在计算一下结果,并且保存到HashMap中
17 result = doCompute(userId);
18 cache.put(userId, result);
19 }
20 return result;
21 }
22
23 private Integer doCompute(String userId) throws InterruptedException {
24 TimeUnit.SECONDS.sleep(5);
25 return new Integer(userId);
26 }
27
28 public static void main(String[] args) throws InterruptedException {
29 ImoocCache1 imoocCache1 = new ImoocCache1();
30 System.out.println("开始计算了");
31 Integer result = imoocCache1.computer("13");
32 System.out.println("第一次计算结果:"+result);
33 result = imoocCache1.computer("13");
34 System.out.println("第二次计算结果:"+result);
35
36 }
37}
第二阶段
- 代码有重构空间——装饰者模式 (在已有方法的基础上,进行一定程度的装饰)
- 假设 ExpensiveFunction 类是耗时计算的实现类,实现了
Computable
接口,但是其本身不具备缓存功能,也不需要考虑缓存的事情。 Computable
接口有一个计算函数 computer,用来代表耗时计算,每个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
Computable 接口
1/**
2 * 描述: 有一个计算函数computer,用来代表耗时计算,每个计算器都要实现这个接口,这样就可以无侵入实现缓存功能
3 */
4public interface Computable <A,V>{
5
6 V compute(A arg) throws Exception;
7}
ExpensiveFunction 耗时计算类
1/**
2 * 描述: 耗时计算的实现类,实现了Computable接口,但是本身不具备缓存能力,不需要考虑缓存的事情
3 */
4public class ExpensiveFunction implements Computable<String, Integer>{
5
6 @Override
7 public Integer compute(String arg) throws Exception {
8 Thread.sleep(5000);
9 return Integer.valueOf(arg);
10 }
11}
ImoocCache2 装饰者类
1/**
2 * 描述: 用装饰者模式,给计算器自动添加缓存功能
3 */
4public class ImoocCache2<A,V> implements Computable<A,V> {
5
6 private final Map<A, V> cache = new HashMap();
7
8 //构造方法传入 Computable 接口的实现类的实例 ExpensiveFunction
9 private final Computable<A,V> c;
10
11 public ImoocCache2(Computable<A, V> c) {
12 this.c = c;
13 }
14
15 /**
16 * 装饰过程 : 实现 Computable 接口,在实现方法中引入 调用 ExpensiveFunction 目标方法,基于此方法做装饰
17 * @param arg
18 * @return
19 * @throws Exception
20 */
21 @Override
22 public synchronized V compute(A arg) throws Exception {
23 System.out.println("进入缓存机制");
24 V result = cache.get(arg);
25 if (result == null) {
26 result = c.compute(arg); //目标方法织入
27 cache.put(arg, result);
28 return result;
29 }
30 System.out.println("命中缓存");
31 return result;
32 }
33
34 public static void main(String[] args) throws Exception {
35 //传入 ExpensiveFunction 实例
36 ImoocCache2<String, Integer> expensiveComputer = new ImoocCache2<>(new ExpensiveFunction());
37 Integer result = expensiveComputer.compute("666");
38 System.out.println("第一次计算结果:"+result);
39 result = expensiveComputer.compute("13");
40 System.out.println("第二次计算结果:"+result);
41 result = expensiveComputer.compute("13");
42 System.out.println("第三次计算结果:"+result);
43 }
44}
1进入缓存机制
2第一次计算结果:666
3进入缓存机制
4第二次计算结果:13
5进入缓存机制
6命中缓存
7第三次计算结果:13
缺点:
1* 在多线程的环境下,已经计算过的 key,如果中间存在新 key,依然需要等待。
2* 多线程的环境下,不同的Key需要等待执行,无法并行。
1 public static void main(String[] args) throws Exception {
2 ImoocCache3<String, Integer> expensiveComputer = new ImoocCache3<>(
3 new ExpensiveFunction());
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 try {
8 Integer result = expensiveComputer.compute("666");
9 System.out.println("第一次的计算结果:"+result);
10 } catch (Exception e) {
11 e.printStackTrace();
12 }
13 }
14 }).start();
15 new Thread(new Runnable() {
16 @Override
17 public void run() {
18 try {
19 Integer result = expensiveComputer.compute("666");
20 System.out.println("第三次的计算结果:"+result);
21 } catch (Exception e) {
22 e.printStackTrace();
23 }
24 }
25 }).start();
26 new Thread(new Runnable() {
27 @Override
28 public void run() {
29 try {
30 Integer result = expensiveComputer.compute("667");
31 System.out.println("第二次的计算结果:"+result);
32 } catch (Exception e) {
33 e.printStackTrace();
34 }
35 }
36 }).start();
37 }
38}
1进入缓存机制
2进入缓存机制
3第一次的计算结果:666
4进入缓存机制
5第二次的计算结果:667
6第三次的计算结果:666
第三阶段
减小锁的粒度。
缺点:
1* 如果在写的过程中读,同样存在线程不安全
2* 如果读操作用synchronized 修饰,那么 读操作和读操作也是互斥的,性能低。
1import com.xdclass.couponapp.test.imooccache.computable.Computable;
2import com.xdclass.couponapp.test.imooccache.computable.ExpensiveFunction;
3import java.util.HashMap;
4import java.util.Map;
5
6/**
7 * 描述: 缩小了synchronized的粒度,提高性能,但是依然并发不安全
8 */
9public class ImoocCache4<A, V> implements Computable<A, V> {
10
11 private final Map<A, V> cache = new HashMap();
12
13 private final Computable<A, V> c;
14
15 public ImoocCache4(Computable<A, V> c) {
16 this.c = c;
17 }
18
19 @Override
20 public V compute(A arg) throws Exception {
21 System.out.println("进入缓存机制");
22 V result = cache.get(arg);
23 if (result == null) {
24 result = c.compute(arg);
25 synchronized (this) {
26 cache.put(arg, result);
27 }
28 }
29 return result;
30 }
31
32 public static void main(String[] args) throws Exception {
33 ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(
34 new ExpensiveFunction());
35 Integer result = expensiveComputer.compute("666");
36 System.out.println("第一次计算结果:" + result);
37 result = expensiveComputer.compute("666");
38 System.out.println("第二次计算结果:" + result);
39 }
40}
第四阶段
虽然提高了并发效率(这里指的是进入目标方法的线程数),但并不意味着就是线程安全的,仅仅用 synchronized
方法修饰写操作是不够的。
没有必要自己手动实现一个安全的 HashMap,即便是自己手动实现了也会因为各方面考虑不到存在性能和安全问题,不如直接使用现有的 ConcurrentHashMap 来进一步优化缓存。
缺点:
1* 在计算完成之前,另一个要求计算相同值的请求到来,会导致计算两遍,这和缓存想避免多次计算的初衷背道而驰,是不可接受的。
2* 如果有100个线程都请求同样的key 的计算,那么大量的重复计算会浪费大量的硬件资源。
1 public static void main(String[] args) throws Exception {
2 ImoocCache6<String, Integer> expensiveComputer = new ImoocCache6<>(
3 new ExpensiveFunction());
4 new Thread(new Runnable() {
5 @Override
6 public void run() {
7 try {
8 Integer result = expensiveComputer.compute("666");
9 System.out.println("第一次的计算结果:" + result);
10 } catch (Exception e) {
11 e.printStackTrace();
12 }
13 }
14 }).start();
15 new Thread(new Runnable() {
16 @Override
17 public void run() {
18 try {
19 Integer result = expensiveComputer.compute("666");
20 System.out.println("第三次的计算结果:" + result);
21 } catch (Exception e) {
22 e.printStackTrace();
23 }
24 }
25 }).start();
26 new Thread(new Runnable() {
27 @Override
28 public void run() {
29 try {
30 Integer result = expensiveComputer.compute("667");
31 System.out.println("第二次的计算结果:" + result);
32 } catch (Exception e) {
33 e.printStackTrace();
34 }
35 }
36 }).start();
37 }
1进入缓存机制
2进入缓存机制
3进入缓存机制
4第一次的计算结果:666
5第三次的计算结果:666
6第二次的计算结果:667
第五阶段
为了避免重复计算,Future、Callable 的妙用。前人种树后人乘凉。
如果后请求的线程可以知道正在计算的线程的计算内容,就可以判断是不是已经存在重复计算,如果是存在于当前线程目标计算内容一样的线程,只需等待即可,就可以避免重复计算。
Value 为 Future 并且立刻可以得到,ConcurrentHashMap 保证多线程之间的可见性
因此当 ConcurrentHashMap 中已存在将要请求的 Key/Value,则会调用 Future.get() 方法阻塞,直到获取到线程结果。
缺点:(虽然概率比较小)
1* 如果有两个同时计算 666 的线程,同时调用 cache.get() 方法,那么返回的结果都为 null,后面还是会创建两个任务去计算相同的值。
1import com.xdclass.couponapp.test.imooccache.computable.Computable;
2import com.xdclass.couponapp.test.imooccache.computable.ExpensiveFunction;
3import java.util.Map;
4import java.util.concurrent.Callable;
5import java.util.concurrent.ConcurrentHashMap;
6import java.util.concurrent.Future;
7import java.util.concurrent.FutureTask;
8
9/**
10 * 描述: 利用Future,避免重复计算
11 */
12public class ImoocCache7<A, V> implements Computable<A, V> {
13
14 private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
15
16 private final Computable<A, V> c;
17
18 public ImoocCache7(Computable<A, V> c) {
19 this.c = c;
20 }
21
22 /**
23 * Value 为 Future 并且立刻可以得到,ConcurrentHashMap 保证多线程之间的可见性
24 * 因此当 ConcurrentHashMap 中已存在将要请求的 Key/Value,则会调用 Future.get() 方法阻塞,直到获取到线程结果。
25 * @param arg
26 * @return
27 * @throws Exception
28 */
29 @Override
30 public V compute(A arg) throws Exception {
31 //多个线程有可能会同时得到 null,存在安全隐患
32 Future<V> f = cache.get(arg);
33 //如果是空:则有可能多个线程同时进入
34 if (f == null) {
35 //Callable
36 Callable<V> callable = new Callable<V>() {
37 @Override
38 public V call() throws Exception {
39 return c.compute(arg);
40 }
41 };
42 //FutureTask
43 FutureTask<V> ft = new FutureTask<>(callable);
44 //Future = FutureTask
45 f = ft;
46 //直接存入Future 作为缓存的 V
47 cache.put(arg, ft);
48 System.out.println("从FutureTask调用了计算函数");
49 //开始计算
50 ft.run();
51 }
52 //得到计算 Future 计算结果
53 return f.get();
54 }
55
56 public static void main(String[] args) throws Exception {
57 ImoocCache7<String, Integer> expensiveComputer = new ImoocCache7<>(
58 new ExpensiveFunction());
59 new Thread(new Runnable() {
60 @Override
61 public void run() {
62 try {
63 Integer result = expensiveComputer.compute("666");
64 System.out.println("第一次的计算结果:" + result);
65 } catch (Exception e) {
66 e.printStackTrace();
67 }
68 }
69 }).start();
70 new Thread(new Runnable() {
71 @Override
72 public void run() {
73 try {
74 Integer result = expensiveComputer.compute("667");
75 System.out.println("第三次的计算结果:" + result);
76 } catch (Exception e) {
77 e.printStackTrace();
78 }
79 }
80 }).start();
81 new Thread(new Runnable() {
82 @Override
83 public void run() {
84 try {
85 Integer result = expensiveComputer.compute("666");
86 System.out.println("第二次的计算结果:" + result);
87 } catch (Exception e) {
88 e.printStackTrace();
89 }
90 }
91 }).start();
92
93 }
94}
1从FutureTask调用了计算函数
2从FutureTask调用了计算函数
3第三次的计算结果:667
4第二次的计算结果:666
5第一次的计算结果:666
第六阶段
原子化 cache.get() 方法。
1import com.xdclass.couponapp.test.imooccache.computable.Computable;
2import com.xdclass.couponapp.test.imooccache.computable.ExpensiveFunction;
3import java.util.Map;
4import java.util.concurrent.Callable;
5import java.util.concurrent.ConcurrentHashMap;
6import java.util.concurrent.Future;
7import java.util.concurrent.FutureTask;
8
9/**
10 * 描述: 利用Future,避免重复计算
11 */
12public class ImoocCache8<A, V> implements Computable<A, V> {
13
14 private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
15
16 private final Computable<A, V> c;
17
18 public ImoocCache8(Computable<A, V> c) {
19 this.c = c;
20 }
21
22 @Override
23 public V compute(A arg) throws Exception {
24 Future<V> f = cache.get(arg);
25 if (f == null) {
26 Callable<V> callable = new Callable<V>() {
27 @Override
28 public V call() throws Exception {
29 return c.compute(arg);
30 }
31 };
32 FutureTask<V> ft = new FutureTask<>(callable);
33
34 // 如果集合中不存在就存放当前值,并且返回 原有位置的 value,所以第一存会返回 null
35 // 如果已存就不会向容器存放当前的值,并且返回当前位置的值 ,所以会返回 Future
36 f = cache.putIfAbsent(arg, ft);
37 // 如果不存在就进行计算,否则返回当前key对应的Value(Future)
38 if (f == null) {
39 f = ft;
40 System.out.println("从FutureTask调用了计算函数");
41 //进行计算
42 ft.run();
43 }
44 }
45 //否则返回当前key对应的Value(Future)
46 return f.get();
47 }
48
49 public static void main(String[] args) throws Exception {
50 ImoocCache8<String, Integer> expensiveComputer = new ImoocCache8<>(
51 new ExpensiveFunction());
52 new Thread(new Runnable() {
53 @Override
54 public void run() {
55 try {
56 Integer result = expensiveComputer.compute("666");
57 System.out.println("第一次的计算结果:" + result);
58 } catch (Exception e) {
59 e.printStackTrace();
60 }
61 }
62 }).start();
63 new Thread(new Runnable() {
64 @Override
65 public void run() {
66 try {
67 Integer result = expensiveComputer.compute("666");
68 System.out.println("第三次的计算结果:" + result);
69 } catch (Exception e) {
70 e.printStackTrace();
71 }
72 }
73 }).start();
74 new Thread(new Runnable() {
75 @Override
76 public void run() {
77 try {
78 Integer result = expensiveComputer.compute("667");
79 System.out.println("第二次的计算结果:" + result);
80 } catch (Exception e) {
81 e.printStackTrace();
82 }
83 }
84 }).start();
85 }
86}
1从FutureTask调用了计算函数
2从FutureTask调用了计算函数
3第二次的计算结果:667
4第三次的计算结果:666
5第一次的计算结果:666
任务异常处理
3 种异常之所以用不同的 catch 块捕获,是因为它们的处理逻辑是不同的。
1* CancellationException、InterruptedException 是人为取消的,那么应该立即终止任务
2* 但是如果是计算错误,并且明确知道多试几次就可以得到正确结果,那么逻辑应该是重试,而不应该是抛出异常。
3* while(true) 来保证计算出错不会影响逻辑,会进入下次循环(重试),如果是人为取消或中断,那么就立即停止并且抛出异常。
cache.remove(arg); 解决缓存污染问题。
耗时计算的实现类(有概率计算失败)
1/**
2 * 描述: 耗时计算的实现类,有概率计算失败
3 */
4public class MayFail implements Computable<String, Integer>{
5
6 @Override
7 public Integer compute(String arg) throws Exception {
8 double random = Math.random();
9 //模拟50%的情况可能会计算失败,抛出异常
10 if (random > 0.5) {
11 throw new IOException("读取文件出错");
12 }
13 Thread.sleep(3000);
14 return Integer.valueOf(arg);
15 }
16}
Cache 类
1/**
2 * 描述: 利用Future,避免重复计算
3 */
4public class ImoocCache9<A, V> implements Computable<A, V> {
5
6 private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
7
8 private final Computable<A, V> c;
9
10 public ImoocCache9(Computable<A, V> c) {
11 this.c = c;
12 }
13
14 @Override
15 public V compute(A arg) throws InterruptedException, ExecutionException {
16 //如果事计算错误,可以通过循环不断的重试,最后会得到正确的结果。
17 while (true) {
18 Future<V> f = cache.get(arg);
19 if (f == null) {
20 Callable<V> callable = new Callable<V>() {
21 @Override
22 public V call() throws Exception {
23 return c.compute(arg);
24 }
25 };
26 FutureTask<V> ft = new FutureTask<>(callable);
27 f = cache.putIfAbsent(arg, ft);
28 if (f == null) {
29 f = ft;
30 System.out.println("从FutureTask调用了计算函数");
31 ft.run();
32 }
33 }
34 try {
35 return f.get();
36 } catch (CancellationException e) {
37 System.out.println("被取消了");
38 //从缓存中删除(缓存污染)
39 cache.remove(arg);
40 throw e;
41 } catch (InterruptedException e) {
42 //从缓存中删除(缓存污染)
43 cache.remove(arg);
44 throw e;
45 } catch (ExecutionException e) {
46 System.out.println("计算错误,需要重试");
47 //从缓存中删除(缓存污染)
48 cache.remove(arg);
49 }
50 }
51 }
52
53 public static void main(String[] args) throws Exception {
54 //实例改为 MayFail
55 ImoocCache9<String, Integer> expensiveComputer = new ImoocCache9<>(new MayFail());
56 new Thread(new Runnable() {
57 @Override
58 public void run() {
59 try {
60 Integer result = expensiveComputer.compute("666");
61 System.out.println("第一次的计算结果:" + result);
62 } catch (Exception e) {
63 e.printStackTrace();
64 }
65 }
66 }).start();
67 new Thread(new Runnable() {
68 @Override
69 public void run() {
70 try {
71 Integer result = expensiveComputer.compute("666");
72 System.out.println("第三次的计算结果:" + result);
73 } catch (Exception e) {
74 e.printStackTrace();
75 }
76 }
77 }).start();
78 new Thread(new Runnable() {
79 @Override
80 public void run() {
81 try {
82 Integer result = expensiveComputer.compute("667");
83 System.out.println("第二次的计算结果:" + result);
84 } catch (Exception e) {
85 e.printStackTrace();
86 }
87 }
88 }).start();
89
90 //模拟中断
91 //Future<Integer> future = expensiveComputer.cache.get("666");
92 //future.cancel(true);
93 }
94}
1######################### 模拟中断
2从FutureTask调用了计算函数
3被取消了
4从FutureTask调用了计算函数
5被取消了
6java.util.concurrent.CancellationException
7 at java.util.concurrent.FutureTask.report(FutureTask.java:121)
8 at java.util.concurrent.FutureTask.get(FutureTask.java:192)
9 at com.xdclass.couponapp.test.imooccache.ImoocCache9.compute(ImoocCache9.java:48)
10 at com.xdclass.couponapp.test.imooccache.ImoocCache9$3.run(ImoocCache9.java:84)
11 at java.lang.Thread.run(Thread.java:748)
12java.util.concurrent.CancellationException
13 at java.util.concurrent.FutureTask.report(FutureTask.java:121)
14 at java.util.concurrent.FutureTask.get(FutureTask.java:192)
15 at com.xdclass.couponapp.test.imooccache.ImoocCache9.compute(ImoocCache9.java:48)
16 at com.xdclass.couponapp.test.imooccache.ImoocCache9$2.run(ImoocCache9.java:73)
17 at java.lang.Thread.run(Thread.java:748)
18第二次的计算结果:667
19
20######################### 增加循环重试
21从FutureTask调用了计算函数
22从FutureTask调用了计算函数
23计算错误,需要重试
24计算错误,需要重试
25计算错误,需要重试
26从FutureTask调用了计算函数
27从FutureTask调用了计算函数
28从FutureTask调用了计算函数
29计算错误,需要重试
30计算错误,需要重试
31从FutureTask调用了计算函数
32从FutureTask调用了计算函数
33计算错误,需要重试
34从FutureTask调用了计算函数
35计算错误,需要重试
36从FutureTask调用了计算函数
37计算错误,需要重试
38从FutureTask调用了计算函数
39第二次的计算结果:667
40第三次的计算结果:666
41第一次的计算结果:666
缓存过期
出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效,那么带来缓存不一致等问题
缓存定时删除的能力。
1/**
2 * 描述: 出于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效,那么带来缓存不一致等问题
3 */
4public class ImoocCache10<A, V> implements Computable<A, V> {
5
6 private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
7
8 private final Computable<A, V> c;
9
10 //周期任务的线程池
11 public final static ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
12
13 public ImoocCache10(Computable<A, V> c) {
14 this.c = c;
15 }
16
17 @Override
18 public V compute(A arg) throws InterruptedException, ExecutionException {
19 while (true) {
20 Future<V> f = cache.get(arg);
21 if (f == null) {
22 Callable<V> callable = new Callable<V>() {
23 @Override
24 public V call() throws Exception {
25 return c.compute(arg);
26 }
27 };
28 FutureTask<V> ft = new FutureTask<>(callable);
29 f = cache.putIfAbsent(arg, ft);
30 if (f == null) {
31 f = ft;
32 System.out.println("从FutureTask调用了计算函数");
33 ft.run();
34 }
35 }
36 try {
37 return f.get();
38 } catch (CancellationException e) {
39 System.out.println("被取消了");
40 cache.remove(arg);
41 throw e;
42 } catch (InterruptedException e) {
43 cache.remove(arg);
44 throw e;
45 } catch (ExecutionException e) {
46 System.out.println("计算错误,需要重试");
47 cache.remove(arg);
48 }
49 }
50 }
51
52 /**
53 * 方法重载 compute()
54 * @param arg
55 * @param expire
56 * @return
57 * @throws ExecutionException
58 * @throws InterruptedException
59 */
60 public V compute(A arg, long expire) throws ExecutionException, InterruptedException {
61 if (expire>0) {
62 executor.schedule(new Runnable() {
63 @Override
64 public void run() {
65 expire(arg);
66 }
67 }, expire, TimeUnit.MILLISECONDS);
68 }
69 return compute(arg);
70 }
71
72 /**
73 * 清除过期缓存的方法
74 * @param key
75 */
76 public synchronized void expire(A key) {
77 //检查缓存中是否还存在对应的key
78 Future<V> future = cache.get(key);
79 //如果缓存中存在对应的key
80 if (future != null) {
81 //如果缓存的时间设置的特别短,也许计算还没有完成,所以应该判断计算是否已完成
82 //如果任务还没有计算完成,就把任务取消
83 if (!future.isDone()) {
84 System.out.println("Future任务被取消");
85 //取消计算任务
86 future.cancel(true);
87 }
88 System.out.println("过期时间到,缓存被清除");
89 //清除缓存
90 cache.remove(key);
91 }
92 }
93 public static void main(String[] args) throws Exception {
94 ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(
95 new MayFail());
96 new Thread(new Runnable() {
97 @Override
98 public void run() {
99 try {
100 Integer result = expensiveComputer.compute("666",5000L);
101 System.out.println("第一次的计算结果:" + result);
102 } catch (Exception e) {
103 e.printStackTrace();
104 }
105 }
106 }).start();
107 new Thread(new Runnable() {
108 @Override
109 public void run() {
110 try {
111 Integer result = expensiveComputer.compute("666");
112 System.out.println("第三次的计算结果:" + result);
113 } catch (Exception e) {
114 e.printStackTrace();
115 }
116 }
117 }).start();
118 new Thread(new Runnable() {
119 @Override
120 public void run() {
121 try {
122 Integer result = expensiveComputer.compute("667");
123 System.out.println("第二次的计算结果:" + result);
124 } catch (Exception e) {
125 e.printStackTrace();
126 }
127 }
128 }).start();
129
130
131 Thread.sleep(6000L); //已经过期(缓存设置的是 5000L)
132 Integer result = expensiveComputer.compute("666");
133 System.out.println("主线程的计算结果:" + result);
134
135 executor.shutdown();
136 }
137}
1从FutureTask调用了计算函数
2从FutureTask调用了计算函数
3第二次的计算结果:667
4第三次的计算结果:666
5第一次的计算结果:666
6过期时间到,缓存被清除
7从FutureTask调用了计算函数 --- 这里可以看出 '666' 的缓存已失效
8主线程的计算结果:666
高并发访问
1~10000 同时计算,同时设置过期,在同一时间就会出现大量缓存过期,造成缓存雪崩、缓存击穿等高并发下的问题。
为了解决缓存集中过期的问题,可以将缓存过期时间设为随机。
缓存方法改为调用 computeRandomExpire(A arg) 方法即可。
1 /**
2 * 缓存随机过期时间
3 * @param arg
4 * @return
5 * @throws ExecutionException
6 * @throws InterruptedException
7 */
8 public V computeRandomExpire(A arg) throws ExecutionException, InterruptedException {
9 long randomExpire = (long) (Math.random() * 10000);
10 //调用 compute() 方法重载
11 return compute(arg, randomExpire);
12 }
模拟大量请求(观测缓存效果)
集中大量线程在同一时间访问缓存。
1* 利用 CountDownLatch 让大量线程集中,在某一个时间同时达到。
2* 让每个线程都打印准备计算时的时间,并且利用 ThreadLocal
3
1import com.test.imooccache.computable.ExpensiveFunction;
2import java.text.SimpleDateFormat;
3import java.util.Date;
4import java.util.concurrent.CountDownLatch;
5import java.util.concurrent.ExecutionException;
6import java.util.concurrent.ExecutorService;
7import java.util.concurrent.Executors;
8
9/**
10 * 描述: TODO
11 */
12public class ImoocCache12 {
13
14 static ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(
15 new ExpensiveFunction());
16
17 //压力集中:
18 public static CountDownLatch countDownLatch = new CountDownLatch(1);
19
20 public static void main(String[] args) throws InterruptedException {
21 ExecutorService service = Executors.newFixedThreadPool(100);
22
23 //统计时间
24 long start = System.currentTimeMillis();
25
26 for (int i = 0; i < 100; i++) {
27 service.submit(() -> {
28 Integer result = null;
29 try {
30 //等待
31 System.out.println(Thread.currentThread().getName()+"开始等待");
32 countDownLatch.await();
33
34 //放行
35 //获取 用于初始化 SimpleDateFormat
36 SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatter.get();
37 //利用 SimpleDateFormat 格式化当前时间
38 String time = dateFormat.format(new Date());
39 //打印当前时间
40 System.out.println(Thread.currentThread().getName()+" "+time+"被放行");
41 //调用计算函数
42 result = expensiveComputer.compute("666");
43 } catch (InterruptedException e) {
44 e.printStackTrace();
45 } catch (ExecutionException e) {
46 e.printStackTrace();
47 }
48 System.out.println(result);
49 });
50 }
51
52 Thread.sleep(5000);
53 countDownLatch.countDown();
54 service.shutdown();
55
56/* while (!service.isTerminated()) {
57 System.out.println("总耗时:" + (System.currentTimeMillis() - start));
58 }*/
59 }
60}
61
62//避免每个线程都创建 SimpleDateFormat ,线程安全的
63class ThreadSafeFormatter {
64
65 public static ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {
66
67 //每个线程会调用本方法一次,用于初始化 SimpleDateFormat
68 @Override
69 protected SimpleDateFormat initialValue() {
70 return new SimpleDateFormat("mm:ss");
71 }
72
73 //首次调用本方法时,会调用initialValue();后面的调用会返回第一次创建的值
74 @Override
75 public SimpleDateFormat get() {
76 return super.get();
77 }
78 };
79}
1pool-2-thread-3开始等待
2...
3pool-2-thread-99开始等待
4pool-2-thread-100开始等待
5pool-2-thread-7 21:07被放行
6...
7从FutureTask调用了计算函数 //只有一个线程还调用计算函数
8pool-2-thread-1 21:07被放行
9pool-2-thread-47 21:07被放行
10pool-2-thread-40 21:07被放行
11pool-2-thread-88 21:07被放行
12pool-2-thread-100 21:07被放行
13pool-2-thread-80 21:07被放行
14pool-2-thread-54 21:07被放行
15...
16666
17666
18666