簡體   English   中英

遍歷並發集合時的線程安全性

[英]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文檔 ,其中指出:

此類及其視圖和迭代器實現MapIterator接口的所有可選方法。

此代碼應該可以工作(我不知道您的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM