Подтвердить что ты не робот

Является ли ConcurrentHashMap.get() доступным для предыдущего ConcurrentHashMap.put() другим потоком?

Является 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
[email protected]
Have lock: 2    
foo=null
New foo time! 2
[email protected]    

Вторая нить не видит сразу! Зачем? На моей производственной системе больше потоков, и я видел только один поток, первый из которых сразу следует за потоком 1, имеет проблему.

Я даже попытался уменьшить уровень concurrency на 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))
4b9b3361

Ответ 1

Некоторые хорошие ответы здесь, но насколько я могу судить, никто не предоставил канонический ответ на вопрос: "Является ли ConcurrentHashMap.get() доступным для предыдущего ConcurrentHashMap.put() другим потоком", Те, кто сказал "да", не предоставили источник.

Итак: да, это гарантировано. Источник (см. раздел "Свойства согласованности памяти" ):

Действия в потоке перед помещением объекта в любой параллельный сбор произойдут - перед действиями после доступа или удаления этого элемента из коллекции в другом потоке.

Ответ 2

Эта проблема создания дорогостоящего объекта в кеше на основе отказа найти его в кеше является известной проблемой. И, к счастью, это уже было реализовано.

Вы можете использовать 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

Одна вещь, которую следует учитывать, заключается в том, равны ли ваши ключи и имеют ли они одинаковые хэш-коды в оба раза вызова "получить". Если они просто String, то да, здесь не будет проблем. Но поскольку вы не указали общий тип ключей, и вы упустили "незначительные" детали в псевдокоде, мне интересно, используете ли вы другой класс в качестве ключа.

В любом случае вы можете дополнительно зарегистрировать хэш-код ключей, используемых для получения/размещения в потоках 1 и 2. Если они разные, у вас есть ваша проблема. Также обратите внимание, что key1.equals(key2) должно быть истинным; это не то, что вы можете зарегистрировать окончательно, но если ключи не являются окончательными классами, стоит записать их полное имя класса, а затем посмотреть на метод equals() для этого класса/классов, чтобы увидеть, возможно ли, что второй ключ можно считать неравным с первым.

И чтобы ответить на ваш заголовок - да, ConcurrentHashMap.get() гарантированно увидит любой предыдущий put(), где "предыдущий" означает, что существует связь происходит-до между двумя, как указано по модели памяти Java. (Для ConcurrentHashMap, в частности, это, по сути, то, что вы ожидаете, с оговоркой, что вы не сможете сказать, что происходит сначала, если оба потока выполняются "точно в одно и то же время" на разных ядрах., вы обязательно должны увидеть результат put() в потоке 2).

Ответ 4

Если поток помещает значение в параллельную хэш-карту, тогда какой-то другой поток, который извлекает значение для карты, обязательно увидит значения, вставленные предыдущим потоком.

Эта проблема была выяснена в "Java Concurrency на практике" Джошуа Блоха.

Цитата из текста: -

В потокобезопасных библиотечных коллекциях предлагаются следующие безопасные гарантии публикации, даже если javadoc менее чем понятен в данной теме:

  • Размещение ключа или значения в Hashtable, synchronizedMap или Concurrent-Map безопасно публикует его для любого другого потока, который извлекает его из Карты (напрямую или через итератор);

Ответ 5

Я не думаю, что проблема в "ConcurrentHashMap", а скорее где-то в вашем коде или о рассуждениях о вашем коде. Я не могу определить ошибку в коде выше (может быть, мы просто не видим плохую часть?).

Но ответ на ваш вопрос: "Является ли ConcurrentHashMap.get() доступным для предыдущего ConcurrentHashMap.put() другим потоком?" Я взломал небольшую тестовую программу.

Короче: Нет, ConcurrentHashMap в порядке!

Если карта плохо написана, следующая программа shoukd напечатает "Плохой доступ!". по крайней мере время от времени. Он выдает 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

Обновление: putIfAbsent() здесь логически корректно, но не исключает проблему создания Foo только в том случае, если ключ отсутствует. Он всегда создает Foo, даже если он не помещает его на карту. Ответ David Roussel хороший, если вы можете принять зависимость Google Collections в своем приложении.


Может, мне что-то не хватает, но почему вы охраняете карту с помощью Семафора? ConcurrentHashMap (CHM) является потокобезопасным (предполагая, что он безопасно опубликован, который он здесь). Если вы пытаетесь получить атомный "put if if not yet in there", используйте chm. putIfAbsent(). Если вам нужны более усложненные инварианты, где содержимое карты не может измениться, вам, вероятно, нужно будет использовать обычный HashMap и синхронизировать его как обычно.

Чтобы ответить на ваш вопрос более прямо: как только ваш put вернется, значение, которое вы положили на карту, гарантировано будет видно в следующем потоке, который его ищет.

Боковое замечание, всего лишь +1 к некоторым другим комментариям о том, чтобы окончательно положить выпуск семафора.

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

Ответ 7

Мы видим интересное проявление модели памяти Java? При каких условиях регистры попадают в основную память? Я думаю, что это гарантировало, что если два потока синхронизируются на одном и том же объекте, тогда они будут видеть согласованное представление в памяти.

Я не знаю, что делает Semphore внутри, почти очевидно, что нужно выполнить некоторую синхронизацию, но знаем ли мы это?

Что произойдет, если вы сделаете

synchronize(dedicatedLockObject)

вместо приобретения семафора?

Ответ 8

Почему вы блокируете параллельную карту хэша? По определению. его нить безопасна. Если есть проблема, ее в вашем коде блокировки. Вот почему у нас есть поточно-безопасные пакеты в Java Лучший способ отладить это - с барьерной синхронизацией.