ConcurrentHashMap.computeIfAbsent threadsafe中的赋值是什么?

时间:2018-04-20 15:35:21

标签: java thread-safety

考虑以下某种固定大小缓存的实现,它允许通过整数句柄进行查找:

static class HandleCache {
    private final AtomicInteger counter = new AtomicInteger();
    private final Map<Data, Integer> handles = new ConcurrentHashMap<>();
    private final Data[] array = new Data[100_000];

    int getHandle(Data data) {
        return handles.computeIfAbsent(data, k -> {
            int i = counter.getAndIncrement();
            if (i >= array.length) {
                throw new IllegalStateException("array overflow");
            }
            array[i] = data;
            return i;
        });

    }

    Data getData(int handle) {
        return array[handle];
    }
}

计算功能中有一个数组存储,它不会以任何方式同步。其他线程的java内存模型是否允许稍后从此数组中读取空值?

PS:如果从getHandle返回的ID存储在final字段中并且只能通过其他线程中的此字段访问,结果是否会更改?

3 个答案:

答案 0 :(得分:1)

读访问不是线程安全的。你可以间接使它保持线程安全,但它可能很脆弱。我会以更简单的方式实现它,并且只有在证明性能问题时才对其进行优化。例如因为你在一个分析器中看到它可以进行真实的测试。

static class HandleCache {
    private final Map<Data, Integer> handles = new HashMap<>();
    private final List<Data> dataByIndex = new ArrayList<>();

    synchronized int getHandle(Data data) {
        Integer id = handles.get(data);
        if (id == null) {
             id = handles.size();
             handles.put(data, id);
             dataByIndex.add(id);
        }
        return id;
    }

    synchronized Data getData(int handle) {
        return dataByIndex.get(handle);
    }
}

答案 1 :(得分:1)

假设您确定从counter的值读取的数组的索引比是 - 您可能会得到空读

最简单的例子(还有其他例子)如下:

T1拨打getHandle(data)并在int i = counter.getAndIncrement();之后暂停 T2调用handles[counter.get()]并读取空值。

您应该可以通过策略性地sleep和两个线程轻松验证这一点。

答案 2 :(得分:0)

来自ConcurrentHashMap#computeIfAbsent的文档:

  

整个方法调用是以原子方式执行的,因此每个键最多应用一次该函数。其他线程在此映射上的某些尝试更新操作可能会在计算进行时被阻止,因此计算应该简短,并且不得尝试更新此映射的任何其他映射。

文档对阻塞的引用仅涉及Map上的更新操作,因此如果任何其他线程尝试直接访问array(而不是通过Map上的更新操作) ,可以有种族条件,可以阅读null