从0到1打造高性能缓存
1、从最简单版缓存入手——HashMap
给HashMap
加final
关键字,属性被声明为final
后,该变量则只能被赋值一次。且一旦被赋值,final
的变量就不能再被改变。所以我们把它加上final关键字,增强安全性。
public class ImoocCache1 {
private final HashMap<String, Integer> cache = new HashMap<>();
public synchronized Integer computer(String userId) throws InterruptedException {
//先检查HashMap里面有没有保存过之前的计算结果
Integer result = cache.get(userId);
if (result == null) {
//如果缓存中找不到,那么需要现在计算一下结果,并且保存到HashMap中
result = doCompute(userId);
cache.put(userId, result);
}
return result;
}
private Integer doCompute(String userId) throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
return new Integer((userId));
}
public static void main(String[] args) throws InterruptedException {
ImoocCache1 imoocCache1 = new ImoocCache1();
System.out.println("开始计算了");
Integer result = imoocCache1.computer("13");
System.out.println("第一次计算结果:"+result);
result = imoocCache1.computer("13");
System.out.println("第二次计算结果:"+result);
}
}
并发安全要保证,用synchronized
实现;
问题:1、性能差;2、代码复用能力差;
2、代码有重构空间——用装饰者模式
我们假设ExpensiveFunction
类是耗时计算的实现类,实现了Computable
接口,但是其本身不具备缓存功能,也不需要考虑缓存的事情。
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);
}
}
问题:性能差;
3、性能待优化——引出锁性能优化经验:缩小锁的粒度
public class ImoocCache3<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new HashMap();
private final Computable<A, V> c;
public ImoocCache3(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 {
ImoocCache3<String, Integer> expensiveComputer = new ImoocCache3<>(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();
}
}
缩小了synchronized的粒度,提高性能,但是依然并发不安全
public class ImoocCache4<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new HashMap();
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("进入缓存机制");
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
synchronized (this) {
cache.put(arg, result);
}
}
return result;
}
public static void main(String[] args) throws Exception {
ImoocCache4<String, Integer> expensiveComputer = new ImoocCache4<>(new ExpensiveFunction());
Integer result = expensiveComputer.compute("666");
System.out.println("第一次计算结果:" + result);
result = expensiveComputer.compute("666");
System.out.println("第二次计算结果:" + result);
}
}
1、虽然提高了并发效率,但是并不意味着就是线程安全的,还需要考虑到同时读写等情况;
2、但是其实没必要自己实现线程安全的HashMap
,也不应该加synchronized
,因为我们自己实现的性能远不如现有的并发集合;
3、使用ConcurrentHashMap
优化缓存
4、用并发集合——ConcurrentHashMap
public class ImoocCache5<A, V> implements Computable<A, V> {
private final Map<A, 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 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 {
ImoocCache5<String, Integer> expensiveComputer = new ImoocCache5<>(new ExpensiveFunction());
Integer result = expensiveComputer.compute("666");
System.out.println("第一次计算结果:" + result);
result = expensiveComputer.compute("666");
System.out.println("第二次计算结果:" + result);
}
}
问题:在计算完成前,另一个要求计算相同值的请求到来,会导致计算两边,这和缓存想避免多次就散的初衷恰恰相反,是不可接受的。
演示重复计算
public class ImoocCache6<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public ImoocCache6(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 {
ImoocCache6<String, Integer> expensiveComputer = new ImoocCache6<>(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();
}
}
5、避免重复计算——Future和Callable的妙用
动机:现在不同的线程进来以后,确实可以同时计算,但是如果两个线程前后脚,也就是相差无几的进来请求同一个数据,
那么我们来看看会出现什么问题:重复计算;如果线程巨大那么会造成巨大的浪费。
改进放向:前人种树,后人乘凉
避免重复计算:
public class ImoocCache7<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public ImoocCache7(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws Exception {
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 = ft;
//在计算之前,把ft放到缓存中,好处就是一旦第一个线程放进缓存中
//第二个在cache.get(arg)的时候,由于ConcurrentHashMap可见性保证,就会立刻看到
// f肯定不为空
cache.put(arg, ft);
System.out.println("从FutureTask调用了计算函数");
ft.run(); //计算
}
return f.get();
}
public static void main(String[] args) throws Exception {
ImoocCache7<String, Integer> expensiveComputer = new ImoocCache7<>(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("667");
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();
}
}
6、依然存在重复的可能——用原子操作putIfAbsent
public class ImoocCache8<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public ImoocCache8(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws Exception {
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(); //计算
}
}
return f.get();
}
public static void main(String[] args) throws Exception {
ImoocCache8<String, Integer> expensiveComputer = new ImoocCache8<>(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("667");
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();
}
}
7、计算中抛出异常——ExecutionException
计算过程并不是一帆风顺的,假设有一个计算类,它有一定概率计算失败,应该如何处理?
8、缓存污染——计算失败则移除Future,增加健壮性
public class ImoocCache9<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public ImoocCache9(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);
}
}
}
public static void main(String[] args) throws Exception {
ImoocCache9<String, Integer> expensiveComputer = new ImoocCache9<>(
new MayFail());
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();
}
}
9、缓存过期功能
为每个结果指定过期时间,并定期扫描过期的元素。
于安全性考虑,缓存需要设置有效期,到期自动失效,否则如果缓存一直不失效,那么会带来缓存不一致等问题。
public class ImoocCache10<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public ImoocCache10(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);
}
}
}
public V computeRandomExpire(A arg) throws ExecutionException, InterruptedException {
long randomExpire = (long) (Math.random() * 10000);
return compute(arg, randomExpire);
}
public final static ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
public V compute(A arg, long expire) throws ExecutionException, InterruptedException {
if (expire>0) {
executor.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 static void main(String[] args) throws Exception {
ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(
new MayFail());
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer result = expensiveComputer.compute("666",5000L);
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();
Thread.sleep(6000L);
Integer result = expensiveComputer.compute("666");
System.out.println("主线程的计算结果:" + result);
}
}
10、高并发访问时
如果同时过期,那么同时都拿不到缓存,导致打爆cpu和MySql,造成缓存雪崩、缓存击穿等高并发下的缓存问题。
解决:缓存过期时间设置为随机。
ImoocCache10.java
11、测试并发性能,所有线程同时访问缓存
ImoocCache11.java
-> ImoocCache12.java
前一个类存在一个问题,就是大量的请求实际上不是同时到达的,而是分先后,但是这样就没办法给缓存造成压力,我们需要真正的同一时刻大量请求到达,此时可以用CountDownLatch来实现。
每个线程都有存储独立信息的需求:ThreadLocal
public class ImoocCache11 {
static ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(new ExpensiveFunction());
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(100);
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
service.submit(() -> {
Integer result = null;
try {
result = expensiveComputer.compute("666");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
});
}
service.shutdown();
while (!service.isTerminated()) {
}
System.out.println("总耗时:" + (System.currentTimeMillis() - start));
}
}
public class ImoocCache12 {
static ImoocCache10<String, Integer> expensiveComputer = new ImoocCache10<>(new ExpensiveFunction());
//倒数值设为1
public static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
service.submit(() -> {
Integer result = null;
try {
System.out.println(Thread.currentThread().getName() + "开始等待");
countDownLatch.await();
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatter.get();
String time = dateFormat.format(new Date());
System.out.println(Thread.currentThread().getName() + " " + time + " 被放行");
result = expensiveComputer.compute("666");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(result);
});
}
Thread.sleep(5000);
countDownLatch.countDown();
service.shutdown();
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatter = new ThreadLocal<SimpleDateFormat>() {
//每个线程会调用本方法一次,用于初始化
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("mm:ss");
}
//首次调用本方法时,会调用initialValue();后面的调用会返回第一次创建的值
@Override
public SimpleDateFormat get() {
return super.get();
}
};
}