Почему i ++ не является атомарным?

97

Почему 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();
    }
}
Andie2302
источник
14
Почему бы тебе не попробовать AtomicInteger?
Braj
3
JVM имеет iincоперацию для увеличения целых чисел, но она работает только для локальных переменных, где параллелизм не является проблемой. Для полей компилятор генерирует команды чтения-изменения-записи отдельно.
Silly Freak
14
Почему вы вообще ожидали, что он будет атомарным?
Hot Licks
2
@Silly Freak: даже если была iincинструкция для полей, наличие единственной инструкции не гарантирует атомарности, например, не- volatile longи doubleдоступ к полю не гарантированно будет атомарным, независимо от того, что он выполняется одной инструкцией байт-кода.
Хольгер

Ответы:

125

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, вам нужно написать программный цикл, который использует lland sc: load-connected и store-conditional. Load-connected читает слово, а store-conditional сохраняет новое значение, если слово не изменилось, или же оно не выполняется (что обнаруживается и вызывает повторную попытку).

Каз
источник
2
поскольку java не имеет указателей, увеличение локальных переменных по своей сути является сохранением потока, поэтому с циклами проблема в основном не будет такой серьезной. ваша точка зрения о наименьшем удивлении, конечно, стоит. также, как есть, i = i + 1будет перевод для ++i, а неi++
глупый урод
22
Первое слово вопроса - «почему». На данный момент это единственный ответ на вопрос «почему». Другие ответы на самом деле просто повторяют вопрос. Итак, +1.
Давуд ибн Карим
3
Возможно, стоит отметить, что гарантия атомарности не решит проблему видимости для обновлений неполя volatile. Поэтому, если вы не будете обрабатывать каждое поле как неявно volatileпосле того, как один поток использовал в нем ++оператор, такая гарантия атомарности не решит проблемы одновременного обновления. Так зачем тратить производительность на что-то, если это не решает проблему.
Хольгер
1
@DavidWallace, не так ли ++? ;)
Дэн Хлавенка
36

i++ включает две операции:

  1. прочитать текущее значение i
  2. увеличить значение и присвоить его 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
Эран
источник
(Даже если бы оно i++ было атомарным, это не было бы четко определенным / поточно-
ориентированным
15
+1, но «1. A, 2. B и C» звучит как три операции, а не две. :)
yshavit 06
3
Обратите внимание, что даже если операция была реализована с помощью одной машинной инструкции, которая увеличивала место хранения на месте, нет гарантии, что она будет поточно-ориентированной. Машине по-прежнему необходимо получить значение, увеличить его и сохранить обратно, плюс может быть несколько копий кеша этого места хранения.
Hot Licks
3
@Aquarelle - если два процессора одновременно выполняют одну и ту же операцию с одним и тем же местом хранения, и в этом месте нет «резервной» трансляции, то они почти наверняка будут мешать и давать ложные результаты. Да, эта операция может быть «безопасной», но требует особых усилий даже на аппаратном уровне.
Hot Licks
6
Но я думаю, что вопрос был «почему», а не «что происходит».
Себастьян Мах
11

Важным является 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 )

Джонатан Розенн
источник
5

Почему i ++ не является атомарным в Java?

Разобьем операцию приращения на несколько операторов:

Потоки 1 и 2:

  1. Получить значение итога из памяти
  2. Добавьте 1 к значению
  3. Напишите обратно в память

Если синхронизации нет, то предположим, что первый поток прочитал значение 3 и увеличил его до 4, но не записал его обратно. В этот момент происходит переключение контекста. Второй поток считывает значение 3, увеличивает его, и происходит переключение контекста. Хотя оба потока увеличили общее значение, оно все равно будет 4 - состояние гонки.

Аникет Такур
источник
2
Я не понимаю, как это должно быть ответом на вопрос. В языке можно определить любую функцию как атомарную, будь то приращения или единороги. Вы просто иллюстрируете последствия отсутствия атомарности.
Себастьян Мах
Да, язык может определять любую функцию как атомарную, но поскольку java считается оператором приращения (который является вопросом, опубликованным OP), не является атомарным, и в моем ответе указаны причины.
Аникет Такур,
1
(извините за мой резкий тон в первом комментарии) Но тогда причина, похоже, в том, «потому что, если бы он был атомарным, то не было бы условий гонки». То есть звучит так, как будто условие гонки желательно.
Себастьян Мах,
@phresnel накладные расходы, вводимые для сохранения атомарного приращения, огромны и редко желательны, что делает операцию дешевой и, как следствие, неатомарной в большинстве случаев.
josefx
4
@josefx: Обратите внимание, что я сомневаюсь не в фактах, а в доводах этого ответа. По сути, он говорит, что «i ++ не является атомарным в Java из-за условий гонки, которые у него есть» , что похоже на высказывание «у машины нет подушки безопасности из-за возможных аварий» или «вы не получите ножа с вашим карривурст-приказом, потому что может потребоваться обрезка колбасных изделий " . Таким образом, я не думаю, что это ответ. Вопрос был не в том, "Что делает i ++?" или "Каковы последствия того, что i ++ не синхронизируется?" .
Себастьян Мах
5

i++ это инструкция, которая просто включает в себя 3 операции:

  1. Прочитать текущее значение
  2. Напишите новое значение
  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нужно, и в вашем случае это, вероятно, подойдет.

PS

Отличная книга, охватывающая все эти вопросы, а также некоторые из них: Java Concurrency in Practice

kstratis
источник
1
Хм. В языке можно определить любую функцию как атомарную, будь то приращения или единороги. Вы просто иллюстрируете последствия отсутствия атомарности.
Себастьян Мах
@phresnel Совершенно верно. Но я также отмечаю, что это не одна операция, которая, в свою очередь, подразумевает, что вычислительные затраты на преобразование нескольких таких операций в атомарные намного дороже, что, в свою очередь, частично оправдывает, почему i++не является атомарной.
kstratis 07
1
Хотя я понимаю вашу точку зрения, ваш ответ немного сбивает с толку. Я вижу пример и вывод, в котором говорится «из-за ситуации в примере»; imho, это неполное рассуждение :(
Себастьян Мах
1
@phresnel Может быть, не самый педагогический ответ, но это лучшее, что я могу предложить на данный момент. Надеюсь, это поможет людям, а не запутает их. Однако спасибо за критику. Я постараюсь быть более точным в своих будущих сообщениях.
kstratis
2

В JVM приращение включает чтение и запись, поэтому оно не атомарно.

Celeritas
источник
2

Если бы операция i++была атомарной, у вас не было бы возможности прочитать из нее значение. Это именно то, что вы хотите использовать i++(вместо использования ++i).

Например, посмотрите следующий код:

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

В этом случае мы ожидаем, что вывод будет: 0 (потому что мы публикуем приращение, например, сначала считываем, а затем обновляем)

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

Другая важная причина заключается в том, что выполнение чего-либо атомарно обычно занимает больше времени из-за блокировки. Было бы глупо, если бы все операции с примитивами занимали немного больше времени в тех редких случаях, когда люди хотят иметь атомарные операции. Вот почему они добавили в язык AtomicIntegerи другие атомарные классы.

Рой ван Рейн
источник
2
Это заблуждение. Вы должны разделить выполнение и получение результата, иначе вы не сможете получить значения из любой атомарной операции.
Себастьян Мах
Нет, это не так, поэтому в Java AtomicInteger есть get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), DecmentAndGet () и т. Д.
Рой ван Рейн,
1
И язык Java можно было i++бы расширить до i.getAndIncrement(). Такое расширение не ново. Например, лямбды в C ++ расширены до определений анонимных классов в C ++.
Себастьян Мах,
Учитывая атомарность, i++можно легко создать атомарную ++iили наоборот. Один эквивалентен другому плюс один.
Дэвид Шварц
2

Есть два шага:

  1. достать я из памяти
  2. установить i + 1 на i

так что это не атомарная операция. Когда thread1 выполняет i ++, а thread2 выполняет i ++, окончательное значение i может быть i + 1.

Янхаогн
источник
-1

Параллелизм ( Threadкласс и тому подобное) - это дополнительная функция в версии 1.0 Java . i++был добавлен в бета-версию до этого, и поэтому он все еще более чем вероятен в своей (более или менее) исходной реализации.

Программист должен синхронизировать переменные. Ознакомьтесь с руководством Oracle по этому поводу .

Изменить: чтобы уточнить, i ++ - это четко определенная процедура, которая предшествует Java, и поэтому разработчики Java решили сохранить исходную функциональность этой процедуры.

Оператор ++ был определен в B (1969), который появился чуть раньше, чем java и потоки.

Летучая мышь
источник
-1 "Тема открытого класса ... Начиная с: JDK1.0" Источник: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Глупый урод,
Версия не так важна, как тот факт, что она все еще была реализована до класса Thread и не была изменена из-за этого, но я отредактировал свой ответ, чтобы доставить вам удовольствие.
TheBat 06
5
Важно то, что ваше утверждение «это все еще было реализовано до класса Thread» не подтверждено источниками. i++не быть атомарным - это дизайнерское решение, а не оплошность растущей системы.
Silly Freak
Лол, это мило. i ++ был определен задолго до Threads, просто потому, что существовали языки, существовавшие до Java. Создатели Java использовали эти другие языки как основу вместо того, чтобы пересмотреть общепринятую процедуру. Где я вообще сказал, что это недосмотр?
TheBat 07
@SillyFreak Вот несколько источников, которые показывают, сколько лет ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat