Почему я ++ не является атомарным?
Почему i++
не является атомарным в Java?
Чтобы получить немного глубже в Java, я пытался подсчитать, как часто выполняется цикл в потоках.
Итак, я использовал
private static int total = 0;
в основном классе.
У меня есть два потока.
- Тема 1: Печать
System.out.println("Hello from Thread 1!");
- Тема 2: Печать
System.out.println("Hello from Thread 2!");
И я подсчитываю строки, напечатанные нитью 1 и нитью 2. Но линии нитки 1 + линии потока 2 не соответствуют общему количеству распечатанных строк.
Вот мой код:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Test {
private static int total = 0;
private static int countT1 = 0;
private static int countT2 = 0;
private boolean run = true;
public Test() {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
newCachedThreadPool.execute(t1);
newCachedThreadPool.execute(t2);
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
run = false;
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
System.out.println((countT1 + countT2 + " == " + total));
}
private Runnable t1 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT1++;
System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
}
}
};
private Runnable t2 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT2++;
System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
}
}
};
public static void main(String[] args) {
new Test();
}
}
Ответы
Ответ 1
i++
, вероятно, не является атомарным в Java, потому что атомарность является особым требованием, которого нет в большинстве применений i++
. Это требование имеет значительные накладные расходы: существует большая стоимость при создании атома прироста; он включает синхронизацию как на программном, так и на аппаратном уровнях, которые не должны присутствовать в обычном приращении.
Вы можете сделать аргумент, что i++
должен был быть спроектирован и задокументирован как специально выполняющий атомный приращение, так что неатомное приращение выполняется с помощью i = i + 1
. Однако это нарушит "культурную совместимость" между Java и C и С++. Кроме того, это уберет удобную нотацию, которую программисты, знакомые с C-подобными языками, считают само собой разумеющимся, придавая ему особое значение, которое применяется только в ограниченных обстоятельствах.
Базовый код C или С++, например for (i = 0; i < LIMIT; i++)
, переводится в Java как for (i = 0; i < LIMIT; i = i + 1)
; потому что было бы нецелесообразно использовать атомный i++
. Что еще хуже, программисты, идущие с C или других C-подобных языков на Java, будут использовать i++
в любом случае, что приведет к ненужному использованию атомных инструкций.
Даже на уровне установки машинных команд операция типа increment обычно не является атомной по соображениям производительности. В x86 специальная инструкция "префикс блокировки" должна использоваться для того, чтобы команда inc
атома: по тем же причинам, что и выше. Если inc
всегда были атомарными, это никогда не будет использоваться, когда требуется неатомный inc; программисты и компиляторы будут генерировать код, который загружает, добавляет 1 и сохраняет, потому что это будет быстрее.
В некоторых архитектурах наборов инструкций нет атома inc
или, возможно, нет inc
вообще; для выполнения атомарного inc на MIPS вам нужно написать программный цикл, который использует привязки ll
и sc
: load-linked и store-conditional. Load-linked читает слово, а store-conditional сохраняет новое значение, если слово не изменилось, иначе оно не сработает (что обнаружено и вызывает повторную попытку).
Ответ 2
i++
включает в себя две операции:
- прочитайте текущее значение
i
- увеличьте значение и назначьте его
i
Когда два потока выполняют i++
в одной и той же переменной одновременно, они могут получать одинаковое текущее значение i
, а затем увеличивать и устанавливать его на i+1
, поэтому вы получите один инкремент вместо двух.
Пример:
int i = 5;
Thread 1 : i++;
// reads value 5
Thread 2 : i++;
// reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
// i == 6 instead of 7
Ответ 3
Важным является JLS (спецификация языка Java), а не то, как различные реализации JVM могут или не могут реализовывать определенную особенность языка. JLS определяет оператор postfix ++ в пункте 15.14.2, который гласит i.a. "значение 1 добавляется к значению переменной, и сумма сохраняется в переменной". Нигде это не упоминается или не намекает на многопоточность или атомарность. Для них JLS обеспечивает нестабильность и синхронизацию. Кроме того, существует пакет java.util.concurrent.atomic(см. http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html)
Ответ 4
Почему я ++ не атомарна в Java?
Позвольте разбить операцию приращения на несколько операторов:
Тема 1 и 2:
- Извлечение значения из памяти
- Добавьте 1 к значению
- Запись в память
Если синхронизации нет, то пусть Thread Thread прочитал значение 3 и увеличил его до 4, но не записал его обратно. На этом этапе происходит переключение контекста. Thread two считывает значение 3, увеличивает его и переключается контекст. Хотя оба потока увеличили общее значение, он все равно будет состоять из 4 человек.
Ответ 5
i++
- это оператор, который просто включает в себя 3 операции:
- Прочитать текущее значение
- Введите новое значение
- Сохранить новое значение
Эти три операции не предназначены для выполнения за один шаг, другими словами i++
не является составной. В результате всевозможные вещи могут ошибаться, когда более чем один поток участвует в одной, но не сложной операции.
В качестве примера представьте этот сценарий:
Время 1:
Thread A fetches i
Thread B fetches i
Время 2:
Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i
// At this time thread B seems to be more 'active'. Not only does it overwrite
// its local copy of i but also makes it in time to store -bar- back to
// 'main' memory (i)
Время 3:
Thread A attempts to store -foo- in memory effectively overwriting the -bar-
value (in i) which was just stored by thread B in Time 2.
Thread B has nothing to do here. Its work was done by Time 2. However it was
all for nothing as -bar- was eventually overwritten by another thread.
И у вас это есть. Состояние гонки.
Вот почему i++
не является атомарным. Если бы это было так, то ничего из этого не произошло бы, и каждый fetch-update-store
мог бы произойти атомарно. Именно то, что AtomicInteger
для и в вашем случае, вероятно, будет подходящим.
P.S.
Отличная книга, охватывающая все эти проблемы, а затем некоторые из них:
Java Concurrency на практике
Ответ 6
Есть два шага:
- извлечение из памяти
- установите я + 1 в i
так что это не атомная операция.
Когда thread1 выполняет я ++, а thread2 выполняет я ++, конечное значение я может быть я + 1.
Ответ 7
В JVM приращение включает чтение и запись, поэтому оно не является атомарным.
Ответ 8
Если операция i++
будет атомарной, у вас не будет возможности прочитать значение из нее. Это именно то, что вы хотите сделать, используя i++
(вместо использования ++i
).
Например, посмотрите на следующий код:
public static void main(final String[] args) {
int i = 0;
System.out.println(i++);
}
В этом случае мы ожидаем, что результатом будет: 0
(потому что мы отправляем инкремент, например, сначала читаем, а затем обновляем)
Это одна из причин, по которой операция не может быть атомарной, потому что вам нужно прочитать значение (и сделать что-то с ним) и , а затем обновить значение.
Другая важная причина заключается в том, что выполнение чего-то атомарно обычно занимает больше времени из-за блокировки. Было бы глупо, если бы все операции над примитивами занимали немного больше времени для редких случаев, когда люди хотят иметь атомные операции. Вот почему они добавили AtomicInteger
и другие атомных классов к языку.
Ответ 9
Concurrency (класс Thread
и т.д.) является добавленной функцией в версии 1.0 Java. i++
был добавлен в бета до этого и, как таковой, он все же более чем вероятен в его (более или менее) исходной реализации.
Программист должен синхронизировать переменные. Ознакомьтесь с учебником Oracle по этому вопросу.
Изменить: Чтобы уточнить, я ++ - это хорошо определенная процедура, предшествующая Java, и поэтому разработчики Java решили сохранить исходную функциональность этой процедуры.
Оператор ++ был определен в B (1969), который предшествует java и потоку только с помощью tad.