代码之家  ›  专栏  ›  技术社区  ›  Stu Thompson Helter Scelter

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

  •  19
  • Stu Thompson Helter Scelter  · 技术社区  · 16 年前

    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。 请救我脱离我自己。

    可能相关的高利细节:

    • 四核64位Xeon(DL380 G5)
    • RHEL4 Linux mysvr 2.6.9-78.0.5.ELsmp #1 SMP x86_64 GNU/Linux )
    • 爪哇6 build 1.6.0_07-b06 , 64-Bit Server VM (build 10.0-b23, mixed mode) )
    8 回复  |  直到 10 年前
        1
  •  6
  •   Cowan    15 年前

    这里有一些很好的答案,但据我所知,没有人真正提供一个标准答案来回答这个问题:“ConcurrentHashMap.get()能保证看到不同线程以前的ConcurrentHashMap.put()”。那些说“是”的人还没有提供消息来源。

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

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

        2
  •  10
  •   Bill the Lizard    16 年前

    根据在缓存中找不到对象而在缓存中创建昂贵对象的问题是已知问题。幸运的是,这已经得到了实施。

    你可以使用 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);
                 }
               });
    

    顺便说一句,在您最初的示例中,使用ConcurrentHashMap没有优势,因为您正在锁定每个访问,为什么不在锁定的部分中使用一个普通的HashMap呢?

        3
  •  3
  •   Andrzej Doyle    16 年前

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

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

    为了回答你的题目-是的,concurrenthashmap.get()保证能看到前面的put(),其中“previous”表示 以前发生过 Java内存模型指定的两者之间的关系。(尤其是对于ConcurrentHashMap,这基本上是您所期望的,但需要注意的是,如果两个线程在不同的核心上“完全相同的时间”执行,您可能无法判断哪一个先发生。不过,在您的例子中,您应该可以在线程2中看到put()的结果)。

        4
  •  3
  •   Ankit Kumar    12 年前

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

    Joshua Bloch在“实践中的Java并发”中澄清了这个问题。

    引用文本:

    线程安全库集合提供以下安全发布保证,即使JavaDoc对主题不太清楚:

    • Hashtable , synchronizedMap Concurrent-Map 将它安全地发布到从映射中检索它的任何其他线程(直接或通过迭代器);
        5
  •  2
  •   Arne Deutsch    16 年前

    我认为问题不在“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!");
            }
        }
    }
    
        6
  •  1
  •   overthink    16 年前

    更新: putIfAbsent() 在这里逻辑上是正确的,但并不能避免只在密钥不存在的情况下创建foo的问题。它总是创建foo,即使它最终没有把它放在地图上。大卫·罗塞尔的回答很好,假设你可以接受你的应用程序中对谷歌收藏的依赖。


    也许我遗漏了一些明显的东西,但你为什么要用信号灯保护地图? ConcurrentHashMap (chm)是线程安全的(假设它是安全发布的,它就在这里)。如果你想让原子“如果还没有放进去的话”,就用chm。 putIfAbsent() . 如果需要更复杂的不变量,而映射内容无法更改,则可能需要使用常规哈希映射并像往常一样同步它。

    更直接地回答你的问题:一旦你的put返回,你在map中输入的值将被下一个寻找它的线程看到。

    附带说明,只需对其他一些关于将信号量发布放在finally中的注释加+1即可。

    if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) {
        try {
            // do stuff while holding permit    
        } finally {
            sem.release();
        }
    }
    
        7
  •  0
  •   djna    16 年前

    我们看到Java内存模型有趣的表现形式吗?在什么情况下寄存器被刷新到主存储器?我认为可以保证,如果两个线程在同一个对象上同步,那么它们将看到一致的内存视图。

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

    如果你这样做会怎么样

    synchronize(dedicatedLockObject)
    

    而不是获取信号量?

        8
  •  0
  •   bigtalktheory    15 年前

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