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


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

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

здесь result и parts are double[].

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

4   51   2015-09-20 10:06:29

4 ответа:

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

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

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

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

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

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

есть дополнительная проблема для double тип. В ПСБ ( 17.7) говорит Вот что:

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

"запись и чтение изменчивых длинных и двойных значений всегда атомарны."


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

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

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

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

(обновление - кто-то указал, что гуава дает AtomicDoubleArray класс, так что б быть вариант. На самом деле хороший.)

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

несмотря на то, что нет AtomicDouble или AtomicDoubleArray в Java, вы можете легко создать свой собственный на основе 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. Они оба имеют одинаковый размер в битах, поэтому точность не теряется (за исключением -Нана, но я не думаю, что это важно).

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

Upd: похоже, что я практически повторно реализовал guava AtomicDoubleArray.

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

как уже было сказано, этот код не является потокобезопасным. Одним из возможных решений, чтобы избежать синхронизации в Java-8 является использование new 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();