Параллельный доступ к байтовому массиву в Java с минимальным количеством блокировок
Я пытаюсь уменьшить использование памяти для блокировки объектов сегментированных данных. Смотрите мои вопросы здесь и здесь. Или просто предположим, что у вас есть байтовый массив, и каждые 16 байтов могут (де) сериализироваться в объект. Давайте назовем это "строкой" с длиной строки 16 байтов. Теперь, если вы модифицируете такую строку из потока записи и читаете из нескольких потоков, вам нужна блокировка. А если у вас размер байтового массива 1 МБ (1024*1024), это означает 65536 строк и такое же количество блокировок.
Это слишком много, а также то, что мне нужны гораздо большие байтовые массивы, и я хотел бы уменьшить его до чего-то примерно пропорционального количеству потоков. Моя идея состояла в том, чтобы создать
ConcurrentHashMap<Integer, LockHelper> concurrentMap;
где Integer
является индексом строки, и перед тем, как поток "входит" в строку, он помещает объект блокировки в эту карту (получил эту идею из этого ответа). Но независимо от того, что я обдумываю, я не могу найти подход, который действительно потокобезопасен:
// somewhere else where we need to write or read the row
LockHelper lock1 = new LockHelper();
LockHelper lock = concurrentMap.putIfAbsent(rowIndex, lock1);
lock.addWaitingThread(); // is too late
synchronized(lock) {
try {
// read or write row at rowIndex e.g. writing like
bytes[rowIndex/16] = 1;
bytes[rowIndex/16 + 1] = 2;
// ...
} finally {
if(lock.noThreadsWaiting())
concurrentMap.remove(rowIndex);
}
}
Видите ли вы возможность сделать этот потокобезопасным?
У меня есть ощущение, что это будет выглядеть очень похоже на concurrentMap.compute
contstruct (например, см. этот ответ) или я мог бы даже использовать этот метод?
map.compute(rowIndex, (key, value) -> {
if(value == null)
value = new Object();
synchronized (value) {
// access row
return value;
}
});
map.remove(rowIndex);
Являются ли значения и "синхронизированные" необходимыми вообще, поскольку мы уже знаем, что операция вычисления выполняется атомарно?
// null is forbidden so use the key also as the value to avoid creating additional objects
ConcurrentHashMap<Integer, Integer> map = ...;
// now the row access looks really simple:
map.compute(rowIndex, (key, value) -> {
// access row
return key;
});
map.remove(rowIndex);
Кстати: с тех пор, когда у нас есть это вычисление в Java. С 1.8? Не могу найти это в JavaDocs
Обновление: я нашел очень похожий вопрос здесь с userIds вместо rowIndices, обратите внимание, что вопрос содержит пример с несколькими проблемами, такими как отсутствие final
звонит lock
внутри try-finally
-заказ и отсутствие сокращения карты. Также, похоже, для этой цели есть библиотека JKeyLockManager, но я не думаю, что она поточно-ориентированная.
Обновление 2: решение кажется очень простым, поскольку Николас Филотто указал, как избежать удаления:
map.compute(rowIndex, (key, value) -> {
// access row
return null;
});
Так что это действительно менее интенсивно использует память, НО простую блокировку сегмента с synchronized
по крайней мере на 50% быстрее в моем сценарии.
1 ответ
Является ли значение и
synchronized
нужно вообще, как мы уже знаем, операция вычисления выполняется атомарно?
Я подтверждаю, что не нужно добавлять synchronized
блок в этом случае как compute
Метод делается атомарно, как указано в Javadoc ConcurrentHashMap#compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)
что было добавлено с BiFunction
поскольку Java 8
, Я цитирую:
Попытки вычислить сопоставление для указанного ключа и его текущего сопоставленного значения (или
null
если нет текущего отображения). Весь вызов метода выполняется атомарно. Некоторые попытки обновления на этой карте другими потоками могут быть заблокированы во время вычислений, поэтому вычисления должны быть короткими и простыми и не должны пытаться обновить какие-либо другие сопоставления этогоMap
,
Чего вы пытаетесь достичь с compute
метод может быть полностью атомным, если вы сделаете BiFunction
всегда возвращается null
удалить ключ атомарно тоже так, что все будет сделано атомарно.
map.compute(
rowIndex,
(key, value) -> {
// access row here
return null;
}
);
Таким образом, вы будете полностью полагаться на механизм блокировки ConcurrentHashMap
синхронизировать ваш доступ к вашим строкам.