在并发编程的实际开发中,共享变量缓存的维护是常见需求,既要保证多线程访问时的数据一致性,又要尽可能减少锁竞争带来的性能损耗。线程池可以统一管理线程资源,避免频繁创建销毁线程的开销,读写锁则能区分读操作和写操作的锁粒度,两者配合可以高效实现共享缓存的维护。

核心组件作用说明
线程池
线程池负责承载并发的读写任务,通过核心线程数、最大线程数、队列等参数配置,适配不同的业务并发量级,避免无限制创建线程导致系统资源耗尽。我们可以使用ThreadPoolExecutor自定义线程池参数,也可以使用Executors提供的快捷创建方法,不过生产环境更推荐自定义配置。
读写锁
读写锁ReentrantReadWriteLock分为读锁和写锁,读锁是共享锁,多个线程可以同时获取读锁执行读操作,不会互相阻塞;写锁是排他锁,同一时间只能有一个线程获取写锁执行写操作,且获取写锁时所有读锁都会被阻塞。这种特性非常适合缓存场景,因为缓存的读操作远多于写操作,能大幅减少锁竞争。
整体实现思路
整体设计分为三个部分:
- 定义共享缓存容器,这里使用HashMap作为示例,实际场景可以根据需求替换为其他容器
- 初始化线程池和读写锁,将锁的读锁和写锁实例暴露给任务使用
- 封装缓存的读方法和写方法,读方法加读锁,写方法加写锁,将读写任务提交到线程池执行
完整代码实现
以下是基于Java语言实现的完整示例代码:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
public class CacheManager {
// 共享缓存容器
private final Map<String, Object> cache = new HashMap<>();
// 读写锁实例
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 读锁
private final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 写锁
private final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 自定义线程池
private final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
/**
* 从缓存中读取数据,读操作加读锁
* @param key 缓存键
* @return 缓存值,不存在返回null
*/
public Object get(String key) {
// 提交读任务到线程池
Future<Object> future = threadPool.submit(() -> {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 执行读操作,key:" + key);
// 模拟读操作耗时
Thread.sleep(50);
return cache.get(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
readLock.unlock();
}
});
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
Thread.currentThread().interrupt();
return null;
}
}
/**
* 向缓存中写入数据,写操作加写锁
* @param key 缓存键
* @param value 缓存值
*/
public void put(String key, Object value) {
// 提交写任务到线程池
threadPool.execute(() -> {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 执行写操作,key:" + key + ",value:" + value);
// 模拟写操作耗时
Thread.sleep(100);
cache.put(key, value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock();
}
});
}
/**
* 关闭线程池,释放资源
*/
public void shutdown() {
threadPool.shutdown();
}
// 测试示例
public static void main(String[] args) {
CacheManager cacheManager = new CacheManager();
// 启动10个读线程
for (int i = 0; i < 10; i++) {
int index = i;
new Thread(() -> cacheManager.get("testKey" + index)).start();
}
// 启动3个写线程
for (int i = 0; i < 3; i++) {
int index = i;
new Thread(() -> cacheManager.put("testKey" + index, "value" + index)).start();
}
// 等待所有任务执行完成
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cacheManager.shutdown();
}
}
关键注意事项
- 锁的释放必须放在finally块中,避免任务执行异常导致锁无法释放,引发死锁问题
- 线程池的参数需要根据实际业务的并发量、任务耗时合理配置,核心线程数不是越大越好,需要结合CPU核心数调整
- 如果缓存需要支持过期淘汰、容量限制等高级特性,可以在写锁的逻辑中添加对应的判断逻辑,所有修改缓存的操作都要加写锁保证互斥
- 读操作如果涉及到缓存不存在时回源加载数据的逻辑,需要注意避免缓存击穿问题,可以结合互斥锁或者空值缓存等方式处理
效果验证
运行上述测试代码可以看到,多个读操作可以几乎同时执行,而写操作执行时会阻塞所有读操作和其他写操作,既保证了缓存数据的一致性,又最大化了读操作的并发效率。如果去掉读写锁,多线程同时写缓存会出现数据覆盖的问题,而如果使用普通的排他锁,所有读操作也会串行执行,性能会大幅下降。