使用volatile实现乐观的线程同步控制

时间:2019-11-10
本文章向大家介绍使用volatile实现乐观的线程同步控制,主要包括使用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