一种线程安全的缓存工具实现方式
前言
在多线程环境下,缓存是一个常见的性能优化手段。然而,实现一个线程安全的缓存并不容易,尤其是在高并发场景下,如何避免重复计算、保证数据一致性是一个挑战。
最近在读《Java并发编程实战》时,书中提到了一种基于 ConcurrentHashMap 和 FutureTask 的线程安全缓存实现方式,今天就来分享记录一下。
实现背景
在高并发场景中,缓存的核心作用是避免重复计算。比如,某个计算任务非常耗时,如果多个线程同时请求相同的数据,我们希望只计算一次,后续请求直接使用缓存结果。
然而,实现这样的缓存工具需要考虑以下几个问题:
线程安全:多个线程可能同时访问缓存,如何避免竞态条件?
避免重复计算:如何确保相同的计算任务只执行一次?
异常处理:如果计算任务抛出异常,如何清理缓存并通知调用方?
实现代码
public interface Computable<A, V> {
V compute(A arg) throws InterruptedException;
}
import java.util.Map;
import java.util.concurrent.*;
public class CacheUtils<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> computable;
public CacheUtils(Computable<A, V> computable) {
this.computable = computable;
}
@Override
public V compute(A arg) throws InterruptedException {
while (true) {
Future<V> future = cache.get(arg);
if (future == null) {
Callable<V> eval = () -> computable.compute(arg);
FutureTask<V> futureTask = new FutureTask<>(eval);
future = cache.putIfAbsent(arg, futureTask);
if (future == null) {
future = futureTask;
futureTask.run();
}
}
try {
return future.get();
} catch (CancellationException e) {
// 如果任务被取消,移除缓存中的 Future
cache.remove(arg, future);
} catch (ExecutionException e) {
// 如果计算任务抛出异常,移除缓存中的 Future 并抛出异常
cache.remove(arg, future);
throw new RuntimeException("Computation failed for argument: " + arg, e.getCause());
}
}
}
}
实现思路说明
使用 ConcurrentHashMap
中 putIfAbsent
方法来保证,同一时刻只能有一个线程可以将 FutureTask
放入缓存,从而避免相同任务的重复计算;当计算任务发生异常时,可以及时抛出错误/取消缓存结果。
应用场景
- 该算法没有缓存失效机制,所以只适用于参数固定,结果固定的场景。
- 算法使用的 FutureTask 异步机制,适用于计算结果耗时的操作。
下面举一个,计算阶乘的例子(可以使用备忘录进行优化,此处为了效果直接进行计算)
public class Calculate implements Computable<Integer, Integer> {
@Override
public Integer compute(Integer arg) {
// 模拟复杂计算情况
if (arg == 0 || arg == 1) {
return 1;
}
return arg * compute(arg - 1);
}
}
public class TestApp {
public static void main(String[] args) throws InterruptedException {
CacheUtils<Integer, Integer> cacheUtils = new CacheUtils<>(new Calculate());
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Integer temp1 = cacheUtils.compute(19);
stopWatch.stop();
System.out.printf("首次计算:耗时%s 结果%s%n", stopWatch.getLastTaskTimeMillis(), temp1);
stopWatch.start();
Integer temp2 = cacheUtils.compute(19);
stopWatch.stop();
System.out.printf("命中缓存:耗时%s 结果%s%n", stopWatch.getLastTaskTimeMillis(), temp2);
}
}
本文来自博客园,作者:帅气的涛啊,转载请注明原文链接:https://www.cnblogs.com/handsometaoa/p/18682740
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了