Является ли 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)
)
Ответы
Ответ 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
Лучший способ отладить это - с барьерной синхронизацией.