簡體   English   中英

ConcurrentHashMap.get()是否保證通過不同的線程看到以前的ConcurrentHashMap.put()?

[英]Is ConcurrentHashMap.get() guaranteed to see a previous ConcurrentHashMap.put() by different thread?

ConcurrentHashMap.get() 保證通過不同的線程看到以前的ConcurrentHashMap.put() 我的期望是,並且閱讀JavaDocs似乎表明了這一點,但我99%確信現實是不同的。 在我的生產服務器上,下面似乎正在發生。 (我已經記錄了它。)

偽代碼示例:

static final ConcurrentHashMap map = new ConcurrentHashMap();
//sharedLock is key specific.  One map, many keys.  There is a 1:1 
//      relationship between key and Foo instance.
void doSomething(Semaphore sharedLock) {
    boolean haveLock = sharedLock.tryAcquire(3000, MILLISECONDS);

    if (haveLock) {
        log("Have lock: " + threadId);
        Foo foo = map.get("key");
        log("foo=" + foo);

        if (foo == null) {
            log("New foo time! " + threadId);
            foo = new Foo(); //foo is expensive to instance
            map.put("key", foo);

        } else
            log("Found foo:" + threadId);

        log("foo=" + foo);
        sharedLock.release();

    } else
        log("No lock acquired");
} 

似乎正在發生的事情是這樣的:

Thread 1                          Thread 2
 - request lock                    - request lock
 - have lock                       - blocked waiting for lock
 - get from map, nothing there
 - create new foo
 - place new foo in map
 - logs foo.toString()
 - release lock
 - exit method                     - have lock
                                   - get from map, NOTHING THERE!!! (Why not?)
                                   - create new foo
                                   - place new foo in map
                                   - logs foo.toString()
                                   - release lock
                                   - exit method

所以,我的輸出看起來像這樣:

Have lock: 1    
foo=null
New foo time! 1
foo=foo@cafebabe420
Have lock: 2    
foo=null
New foo time! 2
foo=foo@boof00boo    

第二個線程沒有立即看到放! 為什么? 在我的生產系統上,有更多的線程,我只看到一個線程,第一個緊跟在線程1之后,有一個問題。

我甚至嘗試將ConcurrentHashMap上的並發級別縮減到1,而不是它應該重要。 例如:

static ConcurrentHashMap map = new ConcurrentHashMap(32, 1);

我哪里錯了? 我的期望? 或者我的代碼(真正的軟件,而不是上面的代碼)中有一些錯誤導致了這個問題嗎? 我反復思考過,99%肯定我正確處理鎖定。 我甚至無法理解ConcurrentHashMap或JVM中的錯誤。 請救我自己。

可能相關的Gorey細節:

  • 四核64位Xeon(DL380 G5)
  • RHEL4( Linux mysvr 2.6.9-78.0.5.ELsmp #1 SMP ... x86_64 GNU/Linux
  • Java 6( build 1.6.0_07-b06 64-Bit Server VM (build 10.0-b23, mixed mode)

基於在高速緩存中找不到它而在高速緩存中創建昂貴的創建對象的問題是已知問題。 幸運的是,這已經實施了。

您可以使用Google Collecitons的 MapMaker 您只需給它一個回調來創建您的對象,如果客戶端代碼在地圖中查找並且映射為空,則調用回調並將結果放入映射中。

MapMaker javadocs ......

 ConcurrentMap<Key, Graph> graphs = new MapMaker()
       .concurrencyLevel(32)
       .softKeys()
       .weakValues()
       .expiration(30, TimeUnit.MINUTES)
       .makeComputingMap(
           new Function<Key, Graph>() {
             public Graph apply(Key key) {
               return createExpensiveGraph(key);
             }
           });

順便說一句,在您的原始示例中,使用ConcurrentHashMap沒有任何優勢,因為您鎖定了每個訪問,為什么不在鎖定的部分中使用普通的HashMap?

這里有一些好的答案,但據我所知,沒有人實際上提出了一個問題的規范答案:“ConcurrentHashMap.get()保證通過不同的線程看到以前的ConcurrentHashMap.put()”。 那些說“是”的人沒有提供消息來源。

所以:是的,它是有保證的。 來源 (請參閱“內存一致性屬性”一節):

在將對象放入任何並發集合之前的線程中的操作發生在從另一個線程中的集合訪問或移除該元素之后的操作之前。

需要考慮的一件事是,您的密鑰是否相同,並且在“get”調用的兩個時間都具有相同的哈希碼。 如果他們只是String s然后是,那么這里不會有問題。 但是,由於您沒有給出通用類型的鍵,並且您已經在偽代碼中省略了“不重要”的細節,我想知道您是否使用另一個類作為鍵。

在任何情況下,您可能還需要在線程1和2中另外記錄用於獲取/放置的鍵的哈希碼。如果這些是不同的,那么您就有了問題。 另請注意, key1.equals(key2)必須為true; 這不是你可以明確記錄的東西,但是如果鍵不是最終類,那么值得記錄它們的完全限定類名,然后查看該類/類的equals()方法,看看是否有可能第二個關鍵可以被認為與第一個關鍵不等。

並回答你的標題 - 是的,ConcurrentHashMap.get()保證可以看到任何先前的put(),其中“previous”表示兩者之間存在一個由Java內存模型指定的先發生關系。 (特別是對於ConcurrentHashMap,這基本上是你所期望的,但需要注意的是,如果兩個線程在不同內核的“完全相同的時間”執行,你可能無法判斷哪個線程首先發生。在你的情況下,雖然,你應該在線程2中看到put()的結果。

如果一個線程在並發哈希映射中放入一個值,那么檢索該映射值的其他一些線程將保證看到前一個線程插入的值。

Joshua Bloch在“Java Concurrency in Practice”中闡明了這個問題。

引用文字: -

線程安全的庫集合提供以下安全發布保證,即使javadoc在主題上不明確:

  • HashtablesynchronizedMapConcurrent-Map放置一個鍵或值可以安全地將它發布到從Map中檢索它的任何其他線程(無論是直接還是通過迭代器);

我不認為問題出在“ConcurrentHashMap”中,而是代碼中的某個地方或者有關代碼的推理。 我無法在上面的代碼中發現錯誤(也許我們只是看不到壞的部分?)。

但是要回答你的問題“ConcurrentHashMap.get()是否保證能通過不同的線程看到以前的ConcurrentHashMap.put()?” 我一起攻擊了一個小測試程序。

簡而言之: 不,ConcurrentHashMap沒問題!

如果地圖寫得很糟糕,下面的程序會打印出來“Bad access!” 至少不時。 它拋出100個線程,對上面概述的方法進行100000次調用。 但它打印出“一切正常!”。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class Test {
    private final static ConcurrentHashMap<String, Test> map = new ConcurrentHashMap<String, Test>();
    private final static Semaphore lock = new Semaphore(1);
    private static int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(100);
        List<Callable<Boolean>> testCalls = new ArrayList<Callable<Boolean>>();
        for (int n = 0; n < 100000; n++)
            testCalls.add(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    doSomething(lock);
                    return true;
                }
            });
        pool.invokeAll(testCalls);
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
        System.out.println("All ok!");
    }

    static void doSomething(Semaphore lock) throws InterruptedException {
        boolean haveLock = lock.tryAcquire(3000, TimeUnit.MILLISECONDS);

        if (haveLock) {
            Test foo = map.get("key");
            if (foo == null) {
                foo = new Test();
                map.put("key", new Test());
                if (counter > 0)
                    System.err.println("Bad access!");
                counter++;
            }
            lock.release();
        } else {
            System.err.println("Fail to lock!");
        }
    }
}

更新: putIfAbsent()在邏輯上是正確的,但不能避免在密鑰不存在的情況下僅創建Foo的問題。 它總是創建Foo,即使它最終沒有把它放在地圖中。 David Roussel的答案很好,假設您可以接受應用中的Google Collections依賴項。


也許我錯過了一些明顯的東西,但你為什么要用信號量守護地圖呢? ConcurrentHashMap (CHM)是線程安全的(假設它已安全發布,它就在這里)。 如果你試圖獲得原子“如果還沒有放在那里”,使用chm。 putIfAbsent() 如果您需要更多不相關的不變量,其中地圖內容無法更改,您可能需要使用常規HashMap並像往常一樣進行同步。

更直接地回答你的問題:一旦你的put返回,你在地圖中放置的值肯定會被尋找它的下一個線程看到。

旁注,關於將信號量釋放放在最后的其他一些注釋中只有+1。

if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) {
    try {
        // do stuff while holding permit    
    } finally {
        sem.release();
    }
}

我們是否看到了Java內存模型的有趣表現? 寄存器在什么條件下刷新到主存儲器? 我認為,如果兩個線程在同一個對象上同步,那么它們將保證一致的內存視圖。

我不知道Semphore在內部做了什么,它幾乎顯然必須做一些同步,但我們知道嗎?

如果你這樣做會發生什么

synchronize(dedicatedLockObject)

而不是詢問信號量?

為什么要鎖定並發哈希映射? 通過def。 它的線程安全。 如果有問題,請在鎖定代碼中。 這就是我們在Java中使用線程安全包的原因。調試此問題的最佳方法是使用屏障同步。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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