Почему 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();
}
}
java
multithreading
concurrency
Andie2302
источник
источник
AtomicInteger
?iinc
операцию для увеличения целых чисел, но она работает только для локальных переменных, где параллелизм не является проблемой. Для полей компилятор генерирует команды чтения-изменения-записи отдельно.iinc
инструкция для полей, наличие единственной инструкции не гарантирует атомарности, например, не-volatile
long
иdouble
доступ к полю не гарантированно будет атомарным, независимо от того, что он выполняется одной инструкцией байт-кода.Ответы:
i++
вероятно, не является атомарным в Java, потому что атомарность - это особое требование, которое не присутствует в большинстве случаев использованияi++
. Это требование связано со значительными накладными расходами: создание атомарной операции приращения требует больших затрат; он включает в себя синхронизацию как на программном, так и на аппаратном уровнях, которая не обязательно должна присутствовать в обычном приращении.Вы могли бы привести аргумент, который
i++
должен был быть разработан и задокументирован как конкретно выполняющий атомарное приращение, чтобы неатомарное приращение выполнялось с использованиемi = i + 1
. Однако это нарушило бы «культурную совместимость» между Java, C и C ++. Кроме того, это уберет удобную нотацию, которую программисты, знакомые с C-подобными языками, считают само собой разумеющимся, придав ей особое значение, применимое только в ограниченных обстоятельствах.Базовый код C или C ++ вроде
for (i = 0; i < LIMIT; i++)
бы переводится на Java какfor (i = 0; i < LIMIT; i = i + 1)
; потому что было бы неуместно использовать атомарныйi++
. Что еще хуже, программисты, перешедшие с C или других C-подобных языков на Java, всеi++
равно будут использовать это , что приведет к ненужному использованию атомарных инструкций.Даже на уровне машинного набора команд операция типа приращения обычно не является атомарной из соображений производительности. В x86 необходимо использовать специальную инструкцию «префикс блокировки», чтобы сделать
inc
инструкцию атомарной: по тем же причинам, что и выше. Если бы онinc
всегда был атомарным, он никогда не использовался бы, когда требуется неатомарный inc; программисты и компиляторы будут генерировать код, который загружает, добавляет 1 и сохраняет, потому что это будет намного быстрее.В некоторых архитектурах с наборами инструкций атомарный
inc
элемент отсутствует или, возможно, вообще отсутствуетinc
; чтобы выполнить atomic inc на MIPS, вам нужно написать программный цикл, который используетll
andsc
: load-connected и store-conditional. Load-connected читает слово, а store-conditional сохраняет новое значение, если слово не изменилось, или же оно не выполняется (что обнаруживается и вызывает повторную попытку).источник
i = i + 1
будет перевод для++i
, а неi++
volatile
. Поэтому, если вы не будете обрабатывать каждое поле как неявноvolatile
после того, как один поток использовал в нем++
оператор, такая гарантия атомарности не решит проблемы одновременного обновления. Так зачем тратить производительность на что-то, если это не решает проблему.++
? ;)i++
включает две операции:i
i
Когда два потока одновременно работают
i++
с одной и той же переменной, они могут получить одноi
и то же текущее значение , а затем увеличить и установить его наi+1
, так что вы получите одно приращение вместо двух.Пример :
источник
i++
было атомарным, это не было бы четко определенным / поточно-Важным является JLS (спецификация языка Java), а не то, как различные реализации JVM могут реализовывать или не реализовывать определенные функции языка. JLS определяет постфиксный оператор ++ в пункте 15.14.2, в котором говорится, например, что «значение 1 добавляется к значению переменной, а сумма сохраняется обратно в переменную». Нигде не упоминается и не намекается на многопоточность или атомарность. Для них JLS обеспечивает энергозависимость и синхронизацию . Кроме того, есть пакет java.util.concurrent.atomic (см. Http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )
источник
Разобьем операцию приращения на несколько операторов:
Потоки 1 и 2:
Если синхронизации нет, то предположим, что первый поток прочитал значение 3 и увеличил его до 4, но не записал его обратно. В этот момент происходит переключение контекста. Второй поток считывает значение 3, увеличивает его, и происходит переключение контекста. Хотя оба потока увеличили общее значение, оно все равно будет 4 - состояние гонки.
источник
i++
это инструкция, которая просто включает в себя 3 операции:Эти три операции не предназначены для выполнения за один шаг или, другими словами
i++
, не являются составной операцией. В результате, когда несколько потоков задействованы в одной, но не составной операции, могут пойти не так.Рассмотрим следующий сценарий:
Время 1 :
Время 2 :
Время 3 :
И вот оно. Состояние гонки.
Вот почему
i++
не атомарен. Если бы это было так, ничего из этого не произошло бы, иfetch-update-store
все происходило бы атомарно. Это именно то, чтоAtomicInteger
нужно, и в вашем случае это, вероятно, подойдет.PS
Отличная книга, охватывающая все эти вопросы, а также некоторые из них: Java Concurrency in Practice
источник
i++
не является атомарной.В JVM приращение включает чтение и запись, поэтому оно не атомарно.
источник
Если бы операция
i++
была атомарной, у вас не было бы возможности прочитать из нее значение. Это именно то, что вы хотите использоватьi++
(вместо использования++i
).Например, посмотрите следующий код:
В этом случае мы ожидаем, что вывод будет:
0
(потому что мы публикуем приращение, например, сначала считываем, а затем обновляем)Это одна из причин, по которой операция не может быть атомарной, потому что вам нужно прочитать значение (и что-то с ним сделать), а затем обновить значение.
Другая важная причина заключается в том, что выполнение чего-либо атомарно обычно занимает больше времени из-за блокировки. Было бы глупо, если бы все операции с примитивами занимали немного больше времени в тех редких случаях, когда люди хотят иметь атомарные операции. Вот почему они добавили в язык
AtomicInteger
и другие атомарные классы.источник
i++
бы расширить доi.getAndIncrement()
. Такое расширение не ново. Например, лямбды в C ++ расширены до определений анонимных классов в C ++.i++
можно легко создать атомарную++i
или наоборот. Один эквивалентен другому плюс один.Есть два шага:
так что это не атомарная операция. Когда thread1 выполняет i ++, а thread2 выполняет i ++, окончательное значение i может быть i + 1.
источник
Параллелизм (
Thread
класс и тому подобное) - это дополнительная функция в версии 1.0 Java .i++
был добавлен в бета-версию до этого, и поэтому он все еще более чем вероятен в своей (более или менее) исходной реализации.Программист должен синхронизировать переменные. Ознакомьтесь с руководством Oracle по этому поводу .
Изменить: чтобы уточнить, i ++ - это четко определенная процедура, которая предшествует Java, и поэтому разработчики Java решили сохранить исходную функциональность этой процедуры.
Оператор ++ был определен в B (1969), который появился чуть раньше, чем java и потоки.
источник
i++
не быть атомарным - это дизайнерское решение, а не оплошность растущей системы.