Параллельный доступ к байтовому массиву в 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 синхронизировать ваш доступ к вашим строкам.

Другие вопросы по тегам