Разделение массива бункеров между потоками

У меня есть приложение, которое многопоточно и работает нормально. Тем не менее, он сталкивается с проблемами блокировки блокировок (проверяется моментальным снимком java-стека и видит, что ждет).

Каждый поток расходует объекты из списка и либо отклоняет их, либо помещает в Bin.

Бункты изначально нулевые, поскольку каждый из них может быть дорогим (и потенциально может быть лот).

Код, вызывающий конфликт, выглядит примерно так:

public void addToBin(Bin[] bins, Item item) {
   Bin bin;
   int bin_index = item.bin_index
   synchronized(bins) {
      bin = bins[bin_index];
      if(bin==null) {
        bin = new Bin();
        bins[bin_index] = bin;
      }
   }
   synchronized(bin) {
     bin.add(item);
   }
}

Именно синхронизация в массиве bins является узким местом.

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

public void addToBin(Bin[] bins, Item item) {
   int bin_index = item.bin_index
   Bin bin = bins[bin_index];

   if(bin==null) {
     synchronized(bins) {
        bin = bins[bin_index];
        if(bin==null) {
          bin = new Bin();
          bins[bin_index] = bin;
        }
     }
   }

   synchronized(bin) {
     bin.add(item);
   }
}

Это безопасно и/или есть лучший/более безопасный/более идиоматический способ сделать это?

Ответ 1

Как уже говорилось в ответе Malt, Java уже предоставляет множество незакрепленных структур данных и понятий, которые могут быть использованы для решения этой проблемы. Я хотел бы добавить более подробный пример, используя AtomicReferenceArray:

Предполагая, что bins является AtomicReferenceArray, следующий код выполняет обновление без блокировки в случае записи null:

Bin bin = bins.get(index);
while (bin == null) {
    bin = new Bin();
    if (!bins.compareAndSet(index, null, bin)) {
        // some other thread already set the bin in the meantime
        bin = bins.get(index);
    }
}
// use bin as usual

Начиная с Java 8, для этого есть более элегантное решение:

Bin bin = bins.updateAndGet(index, oldBin -> oldBin == null ? new Bin() : oldBin);
// use bin as usual

Node: версия Java 8 - хотя и не блокирующая - заметно медленнее, чем версия Java 7 выше, из-за того, что updateAndGet всегда будет обновлять массив, даже если значение не изменится. Это может быть или не быть незначительным в зависимости от избыточных затрат для всей операции обновления bin.


Еще одна очень изящная стратегия может состоять в том, чтобы просто заполнить весь массив bins только что созданными экземплярами Bin, прежде чем передавать массив рабочим потокам. Поскольку нити тогда не должны изменять массив, это уменьшит потребности в синхронизации с объектами Bin. Заполнить массив можно легко сделать многопоточным, используя Arrays.parallelSetAll (начиная с Java 8):

Arrays.parallelSetAll(bins, i -> new Bin());

Обновление 2: Если это опция зависит от ожидаемого результата вашего алгоритма: будет ли в конце массив bins заполняться полностью, плотно или просто редко? (В первом случае предварительное заполнение целесообразно. Во втором случае это зависит - так часто. В последнем случае это, вероятно, плохая идея).


Обновление 1: Не используйте блокировку с двойной проверкой! Это не безопасно! Проблема здесь - видимость, а не атомизм. В вашем случае поток чтения может получить частично построенный (следовательно, поврежденный) экземпляр Bin. Подробнее см. http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html.

Ответ 2

Java имеет множество превосходных блокированных параллельных структур данных, поэтому нет необходимости использовать массивы с синхронизациями для этого типа вещей.

ConcurrentSkipListMap - это параллельная, отсортированная, ключевая карта значений. ConcurrentHashMap является одновременным несортированным значением ключа.

Вы можете просто использовать один из них вместо массива. Просто установите ключ карты как индекс Integer, который вы уже используете, и вам хорошо идти.

Также существуют Google ConcurrentLinkedHashMap и Google Guava Cache, которые отлично подходят для хранения упорядоченных данных, и для удаления старых записей.

Ответ 3

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

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

Ответ 4

Если ни один из встроенных классов Java не поможет вам, вы можете просто создать 8 блокировок замков, скажем, binsALock для binsFLock.

Затем разделите bin_index на 8, используйте напоминание, чтобы выбрать блокировку для использования.


Если вы выберете большее число, которое больше, чем количество потоков, которое у вас есть, и используйте блокировку, которая очень быстра, когда она заявлена, тогда вы можете сделать лучше, чем выбрать 8.

Вы также можете получить лучший результат, уменьшив количество используемых вами потоков.