这种嵌套的地图结构对于多线程读取是否安全?

时间:2015-12-29 14:41:54

标签: java multithreading dictionary concurrency

我已经搜索过this onethis one之类的一些SO问题,但我的结论却无法得出真正的结论。所以,我会说出来:

我有以下嵌套的地图结构概念,适用于多线程环境:

Map<Integer, HashMap<String, AtomicInteger>> bufferMap = new ConcurrentHashMap<Integer, HashMap<String, AtomicInteger>>(2);

这个“缓冲区映射”基本上应该存储一些每小时计数器(AtomicInteger s),由特定的String密钥识别/访问。因此,缓冲区映射的Integer键实际上是小时数(0 ... 23)。我只打算“缓冲”电流并预先设定下一个小时。为此,有一个每小时运行一次的计时器任务,并执行维护程序 - 如下所示:

private final Map<Integer, HashMap<String, AtomicInteger>> bufferMap;
...
private final java.util.Timer;
private final java.util.TimerTask task = new TimerTask() {  
@Override
public void run() {
    ....
    HashMap<String, AtomicInteger> counterMap = bufferMap.get(previousHour);

    // now read internalMap's values, and "store/flush" them somewhere
    // at this point no thread but this one should access previous hour data

    initializeNextHourSlot(); // populate new map entry for the next hour with new AtomicInteger(0) values

    bufferMap.remove(previousHour); // clear previous hour, as no longer needed
}
}

现在,多个线程可以随机和/或并行访问此结构,以下列方式增加计数器:

bufferMap.get(currentHour).get(stringKey).incrementAndGet();

由于外部(缓冲区)映射实际上是由不同的(Timer)线程修改而不是读取它的,所以我认为使用ConcurrentHashMap是合乎逻辑的。

但是,我怀疑内部(计数器)地图 ...它将始终由计时器线程提前填充(没有其他线程应该访问它至少一个小时),然后将被访问(只读),如上所示,以增加计数器值。

这是一种线程安全方法吗?如果没有,那么可能是另一种建议的数据结构(和/或方法)?

2 个答案:

答案 0 :(得分:1)

  

这是一种线程安全方法吗?

您无法确定线程何时会运行。即使您在需要之前一小时创建了结构,该过程也可能处于休眠状态并仍然无法运行(理论上)

  

如果没有,那么可能是另一种建议的数据结构(和/或方法)?

更简单的方法是不使用计时器,如果不存在则使用计算。

final AtomicReference<TimedData> ref = new AtomicReference<>();

public void increment(String counter) {
   TimedData td = ref.get();
   long hour = System.currentTimeMillis() / 3_600_000;
   if (td.hour != hour) {
       saveData(td); // use back ground thread if needed.
       if (!ref.compareAndSet(td, new TimedData(hour))
            td = ref.get();
   }

   td.counterMap.get(counter)
                .incrementAndGet();
}

为班级

class TimedData {
    final long hour;
    final Map<String, AtomicInteger> counterMap = new HashMap<>();

    public TimedData(long hour) {
        this.hour = hour;
        // init the counterMap
    }

在这种情况下,后台线程是可选的,它在运行时无关紧要。

答案 1 :(得分:1)

首先,这不安全。您的计时器和编写器可以同时读/写当前小时数据。考虑一下:

bufferMap.get(currentHour).get(stringKey).incrementAndGet();

不是原子的,这意味着你实际上有:

hourlyMap = bufferMap.get(currentHour);
// assume this thread was now suspended by the OS for some time
keyCounter = hourlyMap.get(stringKey);
keyCounter.incrementAndGet();

在我看来,你有两个定时器线程之间可以交换的地图,但除了效率之外,这对上面的问题没有帮助。假设您希望获得所有计数器的连贯快照(这是一个强烈要求),您需要在阅读时排除编写者更新计数器。有一个例子,使用PhaseLock(http://hdrhistogram.org/)在HdrHistogram库记录器类中解决这个确切的问题,来破解你的计数器地图:

public void incKey(String k) {
    long criticalValueAtEnter = recordingPhaser.writerCriticalSectionEnter();
    try {
        activeCounterMap.get(k).incrementAndGet();
    } finally {
        recordingPhaser.writerCriticalSectionExit(criticalValueAtEnter);
    }
}

private void sampleCounters() {
    try {
        recordingPhaser.readerLock();
        // ...swap your maps here...
        recordingPhaser.flipPhase(500000L /* yield in 0.5 msec units if needed */);
    } finally {
        recordingPhaser.readerUnlock();
    }
}

我相信还有其他类似的锁可以用来实现相同的效果。你想要的是许多作家优先于我认为的单一读者。