我有一个ConcurrentHashMap,它被异步更新以镜像数据库中的数据。我试图根据这些数据对数组进行排序,这些数据大部分时间都能正常工作,但如果数据在排序时更新,那么事情就会变得混乱。
我想过复制地图然后用复制的地图排序,但由于我需要排序的频率和地图的大小,这是不可能的。
答案 0 :(得分:4)
我不确定我是否理解你的要求,所以我会处理两个不同的案例。 假设您的异步“更新”操作要求您更新地图中的2个键。 场景1是:如果两个更新中只有一个可见,则发生“排序”操作是可以的。 场景2是:您需要同时显示2个更新或根本不显示(称为原子行为)。
在这种情况下,ConcurrentHashMap
是可以的,看到迭代器保证在修改地图时不会失败。来自ConcurrentHashMap
文档(强调我的):
类似地,Iterators和Enumerations在或从创建迭代器/枚举的某个点返回反映哈希表状态的元素。它们不会抛出ConcurrentModificationException。但是,迭代器被设计为一次只能由一个线程使用。
因此,即使在修改地图时也可以保证您可以遍历地图,而不会因为并发修改而导致迭代崩溃。但是(参见重点)你不能保证所有同时对地图进行的修改都是立即可见的,即使它们只是部分修改,也不是以哪种顺序。
使用ConcurrentHashMap
进一步了解,您无法保证批量操作(putAll
)会以原子方式运行:
对于诸如putAll和clear之类的聚合操作,并发检索可能反映只插入或删除一些条目。
所以我看到了两种处理这种情况的方案,每种情况都需要锁定。
构建“冻结”副本可以帮助您只有这个副本是在锁定所有其他更新的阶段构建的,因为复制地图意味着迭代它,我们的假设是如果我们进行并发修改,那么迭代是不安全的。
这可能看起来像:
ConcurrentMap<String, String> map = new ConcurrentHashMap<String, String>(); //
AtomicReference<Map<String, String>> frozenCopy = new AtomicReference<Map<String, String>>(map);
public void sortOperation() {
sortUsingFrozenCopy();
}
public void updateOperation() {
synchronized (map) { // Exclusive access to the map instance
updateMap();
Map<String, String> newCopy = new HashMap<String, String>();
newCopy.putAll(map); // You build the copy. This is safe thanks to the exclusive access.
frozenCopy.set(newCopy); // And you update the reference to the copy
}
}
这个解决方案可以改进...... 看到你的2个操作(映射读取和映射写入)是完全异步的,可以假设你的读操作不能知道(并且不应该关心)先前写入操作发生0.1秒之前或将发生0.1秒之后。 因此,您的读取操作取决于地图的“冻结副本”,实际每1(或2,或5或10)秒(或更新事件)更新一次,而不是每次都可能是您的情况。
锁定地图而不复制它是一种解决方案。您需要ReadWriteLock
(或Java 8中的StampedLock
)以便可以进行多种排序,并且互斥读写操作。
解决方案2实际上很容易实现。你有像
这样的东西ReadWriteLock lock = new ReentrantReadWriteLock();
public void sortOperation() {
lock.readLock().lock();
// read lock granted, which prevents writeLock to be granted
try {
sort(); // This is safe, nobody can write
} finally {
lock.readLock().unlock();
}
}
public void updateOperation() {
lock.writeLock().lock();
// Write lock granted, no other writeLock (except to myself) can be granted
// nor any readLock
try {
updateMap(); // Nobody is reading, that's OK.
} finally {
lock.writeLock().unlock();
}
}
使用ReadWriteLock
,可以同时进行多次读取,也可以单次写入,但不能进行多次写入,也不能进行读写操作。
您必须考虑使用锁的公平变体的可能性,以便确保每个读写过程最终都有可能被执行,具体取决于您的使用模式。
(注意:如果你使用Locking / synchronized,你的Map可能不需要是并发的,因为写和读操作将是独占的,但这是另一个主题。)