繁体   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