使用volatile实现乐观的线程同步控制
最近项目在进行并发测试的时候发现了一个问题,定位到这样一段代码:
//简化代码如下
private Map<String, Device> deviceMap = new ConcurrentHashMap<>();
/** 取缓存 */
public Device get(String key){
return deviceMap.get(key);
}
/** 数据发生变化时会更新缓存 */
public void cacheDevices(){
...
deviceMap.clear();
deviceMap.putAll(...);
...
}
核心逻辑很简单,就是读写一个简单的本地缓存。这里有一个线程负责更新缓存,多个线程会去取缓存。已知每次更新缓存时都能将所有可能取的数据缓存到上面的集合中,但是在几万的并发访问下还是发生了取不到目标缓存的现象。查到这段代码时不难发现问题所在,由于没有采取线程同步措施,缓存集合在调用clear而还没有完成新的数据的载入的时候,此时调用取缓存方法就会失败,因为此时缓存集合还是空的。
很容易想到使用syncronized关键字来粗暴地解决任何线程同步问题:
private Map<String, Device> deviceMap = new ConcurrentHashMap<>();
/** 取缓存 */
public Device get(String key){
syncronized(deviceMap) {
return deviceMap.get(key);
}
}
/** 数据发生变化时会更新缓存 */
public void cacheDevices(){
...
syncronized(deviceMap) {
deviceMap.clear();
deviceMap.putAll(...);
}
...
}
然而在多线程高并发地读取缓存的情况下,这种线程同步方式对性能的影响太大了。syncronized是“悲观”的,它假设更新很可能冲突,每次都要先获取锁才能去操作要同步的对象,其他线程要进入该同步块的时候就要被阻塞,进入锁等待队列。事实上,在这种读远多于写的场景下,我们应该“乐观”一点,AtomicInteger这样的原子类型就为我们提供了很好的示范:
//JDK1.7 AtomicInteger源码
//volatile保证真实值的内存可见性
private volatile int value;
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
这是原子整型进行自增的方法。这里原子变量的更新逻辑是非阻塞式的,是乐观的,它假设更新冲突比较少,使用循环重试处理冲突,这种同步方式不会阻塞其他线程,在更新较少的情况下性能损失很小,我们可以利用这种思想改造上面的代码:
private Map<String, Device> deviceMap = new ConcurrentHashMap<>();
private volatile boolean updatingCache = false;
/** 取缓存 */
public Device get(String key){
while(true){
if(!updatingCache){
return deviceMap.get(key);
}
}
}
/** 数据发生变化时会更新缓存 */
public void cacheDevices(){
...
updatingCache = true;
deviceMap.clear();
deviceMap.putAll(...);
updatingCache = false;
...
}
这里增加updatingCache变量用于告诉线程当前集合是否正在更新。为了保证updatingCache变量的修改对其他线程立即可见,这里使用volatile进行修饰,volatile变量在更新时会被立即写入主内存(相对于线程私有的工作内存),其他线程在使用该变量时,也必须先从主内存更新该值,由此保证变量的内存可见性。这里要注意只有在更新线程只有一个的情况下这么做才有效,因为volatile只能保证可见性,无法保证操作的原子性,如果有多个线程修改volatile变量,还是要通过加锁来保证操作原子性。
如果更新操作的耗时较长,还可以改造一下,以使读线程适当让出CPU时间分片:
/** 取缓存 */
public Device get(String key){
while(true){
if(!updatingCache){
return deviceMap.get(key);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
...
}
}
}
以上方法只是针对当前代码事实利用简单的Java知识快速解决问题的一种思路,时间充足的情况下还是应该好好研读一下像Guava cache这样主流的cache类库的源码,必要的话直接引入成熟的缓存产品。
原文地址:https://www.cnblogs.com/qingkongxing/p/11830545.html
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 基于飞桨复现CVPR 2016 MCNN的过程解析:教你更精确估算人流密度
- mysql各种引擎对比、实战
- 接球小游戏玩腻了?换个姿势让PaddleX帮你吊打游戏系统
- mysql事务隔离级别详解和实战
- ELK+FileBeat+Kafka分布式系统搭建图文教程
- Flink CEP 原理和案例详解
- 实战开发,使用 Spring Session 与 Spring security 完成网站登录改造!!
- 分布式计算框架Gearman原理详解
- 【从0开始の全记录】Flume+Kafka+Spark+Spring Boot 统计网页访问量项目
- 系统级性能分析工具perf的介绍与使用[转]
- 深入理解排序算法
- 用nginx缓存静态文件
- 优雅的玩PHP多进程
- 聊一聊mycat数据库集群系列之双主双重实现
- Fast-SCNN的解释以及使用Tensorflow 2.0的实现