![](/img/trans.png)
[英]Why check concurrent modifications when iterating thread-unsafe collections?
[英]Thread safety when iterating through concurrent collections
我正在編寫一些必須處理多個線程的客戶端服務器應用程序。 我有一些服務器,每隔幾秒鍾發送一次活動數據包。 這些服務器保存在ConcurrentHashMap中,其中包含它們的端點和相應服務器的最后一個活動包到達的時間。
現在,我有了一個線程,該線程必須“整理”在特定時間內未發送活動數據包的所有服務器。
我想我不能那樣做,可以嗎?
for( IPEndPoint server : this.fileservers.keySet() )
{
Long time = this.fileservers.get( server );
//If server's time is updated here, I got a problem
if( time > fileserverTimeout )
this.fileservers.remove( server );
}
有沒有一種方法可以解決這個問題,而無需為整個循環獲取鎖(然后我還必須在其他線程中也要尊重)?
根據您在地圖中存儲的內容,這里可能沒有問題。 您的代碼對我來說有點奇怪,因為您似乎節省了“服務器未處於活動狀態的持續時間”。
我記錄數據的第一個想法是存儲“服務器處於活動狀態的最新時間戳”。 然后您的代碼將如下所示:
package so3950354;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ServerManager {
private final ConcurrentMap<Server, Long> lastActive = new ConcurrentHashMap<Server, Long>();
/** May be overridden by a special method for testing. */
protected long now() {
return System.currentTimeMillis();
}
public void markActive(Server server) {
lastActive.put(server, Long.valueOf(now()));
}
public void removeInactive(long timeoutMillis) {
final long now = now();
Iterator<Map.Entry<Server, Long>> it = lastActive.entrySet().iterator();
while (it.hasNext()) {
final Map.Entry<Server, Long> entry = it.next();
final long backThen = entry.getValue().longValue();
/*
* Even if some other code updates the timestamp of this server now,
* the server had timed out at some point in time, so it may be
* removed. It's bad luck, but impossible to avoid.
*/
if (now - backThen >= timeoutMillis) {
it.remove();
}
}
}
static class Server {
}
}
如果您真的想避免在調用removeInactive
過程中沒有代碼調用markActive
,則無法繞開顯式鎖定。 您可能想要的是:
markActive
。 markActive
期間, markActive
調用removeInactive
。 removeInactive
期間,不允許調用markActive
。 對於ReadWriteLock
,這看起來很典型,其中markActive
是“讀取”操作, removeInactive
是“寫入”操作。
在您的代碼中,我看不到另一個線程如何更新服務器的時間。 一旦使用this.fileservers.get( server )
從地圖中檢索了服務器的時間,另一個線程就無法更改其值,因為Long對象是不可變的。 是的,另一個線程可以為該服務器添加一個新的Long對象到映射中,但這不會影響該線程,因為它已經獲取了服務器的時間。
這樣看來,您的代碼看不到任何錯誤。 ConcurrentHashMap中的迭代器是弱一致性的 ,這意味着它們可以容忍並發修改,因此也不存在引發ConcurrentModificationException的風險。
(請參閱Roland的答案 ,該答案將此處的思想帶入並充實為一個更完整的示例,並提供了許多其他見解。)
由於它是一個並發的哈希映射,因此您可以執行以下操作。 請注意,CHM的迭代器均實現了所需的可選方法,包括remove()
。 請參閱CHM API文檔 ,其中指出:
此類及其視圖和迭代器實現
Map
和Iterator
接口的所有可選方法。
此代碼應該可以工作(我不知道您的CHM中的Key
類型):
ConcurrentHashMap<K,Long> fileservers = ...;
for(Iterator<Map.Entry<K,Long>> fsIter = fileservers.entrySet().iterator(); fileservers.hasNext(); )
{
Map.Entry<K,Long> thisEntry = fsIter.next();
Long time = thisEntry.getValue();
if( time > fileserverTimeout )
fsIter.remove( server );
}
但是請注意,其他地方可能存在競爭條件...您需要確保訪問該地圖的其他代碼位可以應對這種自發刪除-即,無論您在何處觸摸fileservers.put()
涉及fileservers.putIfAbsent()
的一些邏輯。 該解決方案是不太可能產生瓶頸不是使用synchronized
,但它也需要一些更多的思考。
您寫道“如果服務器時間在這里更新,我有問題”的地方正是putIfAbsent()
進入的地方。如果該條目不存在,則可能是您之前沒有看過它,還是剛從表中刪除了它。 如果需要協調這兩個方面,那么您可能想為該條目引入一個可鎖定的記錄,並在該級別執行同步(即在執行remove()
時同步記錄,而不是在整個桌子)。 這樣, put()
末尾也可以在同一記錄上同步,從而消除了潛在的競爭。
首先使地圖同步
this.fileservers = Collections.synchronizedMap(Map)
然后使用在Singleton類中使用的策略
if( time > fileserverTimeout )
{
synchronized(this.fileservers)
{
if( time > fileserverTimeout )
this.fileservers.remove( server );
}
}
現在,這確保了一旦進入同步塊,就不會發生任何更新。 之所以如此,是因為一旦獲取了地圖上的鎖,map(同步包裝器)將無法自己在其上提供線程鎖以進行更新,刪除等操作。
兩次檢查時間可確保僅在確實存在刪除的情況下才使用同步
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.