ConcurrentHashMap.get()是否保证通过不同的线程看到以前的ConcurrentHashMap.put()?

时间:2009-11-20 12:26:57

标签: java multithreading concurrenthashmap

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,而不是它应该重要。 E.g:

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-b0664-Bit Server VM (build 10.0-b23, mixed mode)

8 个答案:

答案 0 :(得分:10)

基于无法在缓存中找到它而在缓存中创建昂贵的创建对象的问题是已知问题。幸运的是,这已经实施了。

您可以使用MapMaker中的Google Collecitons。你只需给它一个回调来创建你的对象,如果客户端代码在地图中查找并且地图是空的,则调用回调并将结果放在地图中。

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);
             }
           });
BTW,在您的原始示例中,使用ConcurrentHashMap没有任何优势,因为您锁定了每个访问权限,为什么不在锁定的部分中使用普通的HashMap?

答案 1 :(得分:6)

这里有一些好的答案,但据我所知,没有人实际上提出了一个问题的规范答案:“ConcurrentHashMap.get()保证通过不同的线程看到以前的ConcurrentHashMap.put()” 。那些说“是”的人没有提供消息来源。

所以:是的,它是有保证的。 Source(请参阅“内存一致性属性”一节):

  

在将对象放入任何并发集合之前的线程中的操作发生在从另一个线程中的集合访问或删除该元素之后的操作之前。

答案 2 :(得分:3)

如果一个线程在并发哈希映射中放入一个值,那么检索该映射值的其他一些线程将保证看到前一个线程插入的值。

这个问题已在Joshua Bloch的“Java Concurrency in Practice”中得到澄清。

引用文字: -

  

线程安全库集合提供以下安全发布保证,即使javadoc在主题上不明确:

     
      
  • HashtablesynchronizedMapConcurrent-Map中放置一个键或值,可以安全地将其发布到从Map中检索它的任何其他线程(无论是直接还是通过迭代器); < / LI>   

答案 3 :(得分:3)

要考虑的一件事是,您的密钥是否相同,并且在“get”调用的两次都具有相同的哈希码。如果他们只是String,那么是的,这里不会有问题。但是,由于您没有给出密钥的泛型类型,并且您在伪代码中省略了“不重要”的细节,我想知道您是否使用另一个类作为密钥。

在任何情况下,您可能还需要在线程1和2中另外记录用于获取/置入的键的哈希码。如果这些是不同的,那么您就有了问题。另请注意key1.equals(key2)必须为真;这不是你可以明确记录的东西,但是如果键不是最终类,那么值得记录它们的完全限定类名,然后查看该类/类的equals()方法,看看是否有可能可以认为第二个密钥与第一个密钥不相等。

要回答你的标题 - 是的,ConcurrentHashMap.get()保证会看到任何先前的put(),其中“previous”表示两者之间存在发生之前关系由Java内存模型。 (特别是对于ConcurrentHashMap,这基本上是你所期望的,但需要注意的是,如果两个线程在不同内核的“完全相同的时间”执行,你可能无法判断哪个线程首先发生。在你的情况下,虽然,你应该在线程2中看到put()的结果。

答案 4 :(得分:2)

我不认为问题出在“ConcurrentHashMap”中,而是代码中的某个地方或者有关代码的推理。我无法在上面的代码中发现错误(也许我们只是看不到坏的部分?)。

但要回答你的问题“ConcurrentHashMap.get()是否保证能通过不同的线程看到以前的ConcurrentHashMap.put()?”我已经将一个小型测试程序一起攻击了。

简而言之:不,ConcurrentHashMap没问题!

如果地图写得不好,则以下程序会打印出“访问不良!”至少不时。它抛出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!");
        }
    }
}

答案 5 :(得分:1)

更新: 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();
    }
}

答案 6 :(得分:0)

我们是否看到了Java内存模型的有趣表现?寄存器在什么条件下刷新到主存储器?我认为保证如果两个线程在同一个对象上同步,那么它们将看到一致的内存视图。

我不知道Semphore在内部做了什么,它几乎显然必须做一些同步,但我们知道吗?

如果你这样做会怎样?

synchronize(dedicatedLockObject)

而不是获取信号量?

答案 7 :(得分:0)

为什么要锁定并发哈希映射?通过def。它的线程安全。 如果有问题,请在锁定代码中。 这就是我们在Java中使用线程安全包的原因 调试此问题的最佳方法是使用屏障同步。