31

保证看到以前的不同线程?我的期望是,阅读 JavaDocs 似乎表明了这一点,但我 99% 确信现实是不同的。在我的生产服务器上,似乎正在发生以下情况。(我已经通过日志记录了它。)ConcurrentHashMap.get() ConcurrentHashMap.put()

伪代码示例:

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    

第二个线程不会立即看到 put!为什么?在我的生产系统上,有更多线程,我只看到一个线程,即紧随线程 1 的第一个线程有问题。

我什至尝试将 ConcurrentHashMap 上的并发级别缩小到 1,但这并不重要。例如:

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

我哪里错了?我的期望?或者我的代码(真正的软件,不是上面的)中是否有一些错误导致了这种情况?我已经反复检查了它,并且 99% 确定我正确处理了锁定。我什至无法理解ConcurrentHashMapJVM 中的错误。 请救我脱离自己。

可能相关的 Gorey 细节:

  • 四核 64 位至强 (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))
4

8 回答 8

10

这个基于在缓存中找不到对象而在缓存中创建代价高昂的对象的问题是已知问题。幸运的是,这已经实施了。

您可以使用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?

于 2009-11-20T14:20:20.250 回答
9

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

所以:是的,这是有保证的。来源(参见“内存一致性属性”部分):

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

于 2011-05-24T01:21:30.950 回答
4

如果一个线程将一个值放入并发散列映射中,那么检索该映射值的某个其他线程可以保证看到前一个线程插入的值。

Joshua Bloch 在“Java Concurrency in Practice”中阐明了这个问题。

引用文字:-

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

  • 将键或值放在 a 中HashtablesynchronizedMapConcurrent-Map将其安全地发布到从 Map 中检索它的任何其他线程(无论是直接还是通过迭代器);
于 2013-10-06T19:24:48.890 回答
3

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

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

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

于 2009-11-20T14:46:23.240 回答
2

我认为问题不在于“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!");
        }
    }
}
于 2009-11-20T13:06:38.140 回答
1

更新: putIfAbsent()在这里逻辑上是正确的,但不能避免仅在密钥不存在的情况下创建 Foo 的问题。它总是创建 Foo,即使它最终没有将其放入地图中。假设您可以在应用程序中接受 Google Collections 依赖项,David Roussel 的回答很好。


也许我遗漏了一些明显的东西,但你为什么要用信号量守卫地图? ConcurrentHashMap(CHM) 是线程安全的(假设它是安全发布的,它就在这里)。如果你想获得原子“如果还没有放在那里”,请使用 chm。putIfAbsent(). 如果您需要地图内容无法更改的更复杂的不变量,您可能需要使用常规 HashMap 并照常同步它。

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

旁注,只是对有关将信号量发布放在最后的一些其他评论的 +1。

if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) {
    try {
        // do stuff while holding permit    
    } finally {
        sem.release();
    }
}
于 2009-11-20T14:12:01.763 回答
0

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

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

如果你这样做会发生什么

synchronize(dedicatedLockObject)

而不是获取信号量?

于 2009-11-20T12:52:20.213 回答
0

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

于 2011-05-23T22:21:48.777 回答