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

Является ли оператор + = потокобезопасным в Java?

Я нашел следующий код Java.

for (int type = 0; type < typeCount; type++)
    synchronized(result) {
        result[type] += parts[type];
    }
}

где result и parts являются double[].

Я знаю, что основные операции над примитивными типами являются потокобезопасными, но я не уверен в +=. Если выше synchronized необходимо, может быть, лучший класс для обработки такой операции?

4b9b3361

Ответ 1

Нет. Операция += не является потокобезопасной. Это требует блокировки и/или правильной цепочки отношений "бывает раньше" для любого выражения, включающего назначение для общего поля или элемента массива, чтобы быть потокобезопасным.

(С полем, объявленным как volatile, существуют отношения "произойдет-до"... но только при выполнении операций чтения и записи. Операция += состоит из чтения и записи. Это индивидуально атомарные, но последовательность не является. И большинство выражений присваивания с использованием = включают как одно или несколько чтений (справа), так и запись. Эта последовательность также не является атомарной.)

Для полной истории прочитайте JLS 17.4... или соответствующую главу "Java Concurrency in Action" Брайана Гетца и др.

Как я знаю, основные операции над примитивными типами являются потокобезопасными...

Собственно, это неправильная предпосылка:

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

Существует дополнительная проблема для типа double. JLS (17.7) говорит следующее:

"Для целей модели памяти языка программирования Java одна запись в энергонезависимое длинное или двойное значение рассматривается как две отдельные записи: по одной на каждую 32-битную половину. Это может привести к ситуации, когда поток видит первые 32 бита 64-битного значения из одной записи, а второй 32 бита из другой записи."

"Пишет и читает изменчивые длинные и двойные значения, всегда атомарные".


В комментарии вы спросили:

Какой тип я должен использовать, чтобы избежать глобальной синхронизации, которая останавливает все потоки внутри этого цикла?

В этом случае (где вы обновляете double[], нет никакой альтернативы синхронизации с блокировками или примитивными мьютексами.

Если у вас есть int[] или long[], вы можете заменить их на AtomicIntegerArray или AtomicLongArray и использовать обновление без блокировки этих классов. Однако нет класса AtomicDoubleArray или даже класса AtomicDouble.

(UPDATE), кто-то указал, что Guava предоставляет класс AtomicDoubleArray, поэтому это будет вариант. На самом деле хороший.)

Одним из способов избежать "глобальной блокировки" и массивных проблем с конфликтом может быть разделение массива на условные области, каждый со своей собственной блокировкой. Таким образом, одному потоку нужно только блокировать другой поток, если они используют одну и ту же область массива. (Отдельные записи/блокировки нескольких считывателей также могут помочь... если читается подавляющее большинство обращений.)

Ответ 2

Несмотря на то, что в java нет AtomicDouble или AtomicDoubleArray, вы можете легко создать свой собственный на основе AtomicLongArray.

static class AtomicDoubleArray {
    private final AtomicLongArray inner;

    public AtomicDoubleArray(int length) {
        inner = new AtomicLongArray(length);
    }

    public int length() {
        return inner.length();
    }

    public double get(int i) {
        return Double.longBitsToDouble(inner.get(i));
    }

    public void set(int i, double newValue) {
        inner.set(i, Double.doubleToLongBits(newValue));
    }

    public void add(int i, double delta) {
        long prevLong, nextLong;
        do {
            prevLong = inner.get(i);
            nextLong = Double.doubleToLongBits(Double.longBitsToDouble(prevLong) + delta);
        } while (!inner.compareAndSet(i, prevLong, nextLong));
    }
}

Как вы можете видеть, я использую Double.doubleToLongBits и Double.longBitsToDouble для хранения Doubles как Longs в AtomicLongArray. Оба они имеют одинаковый размер в битах, поэтому точность не теряется (кроме -NaN, но я не думаю, что это важно).

В Java 8 реализация add может быть еще проще, так как вы можете использовать accumulateAndGet метод AtomicLongArray, который был добавлен в java 1.8.

Обновление. Похоже, что я практически выполнил повторную реализацию guava AtomicDoubleArray.

Ответ 3

Даже обычный "двойной" тип данных не является потокобезопасным (потому что он не является атомарным) в 32-разрядных JVM, так как он занимает восемь байтов в Java (что включает в себя 2 * 32-разрядные операции).

Ответ 4

Как уже объяснялось, этот код не является потокобезопасным. Одним из возможных решений избежать синхронизации в Java-8 является использование нового класса DoubleAdder, который способен поддерживать сумму двойных чисел в потокобезопасном режиме.

Создайте массив объектов DoubleAdder перед распараллеливанием:

DoubleAdder[] adders = Stream.generate(DoubleAdder::new)
                             .limit(typeCount).toArray(DoubleAdder[]::new);

Затем скопируйте сумму в параллельные потоки следующим образом:

for(int type = 0; type < typeCount; type++) 
    adders[type].add(parts[type]);
}

Наконец, получим результат после завершения параллельных подзадач:

double[] result = Arrays.stream(adders).mapToDouble(DoubleAdder::sum).toArray();