Почему 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();
    }
}

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


1
Хм. В языке можно определить любую функцию как атомарную, будь то приращения или единороги. Вы просто иллюстрируете последствия отсутствия атомарности.
— Себастьян Мах

@phresnel Совершенно верно. Но я также отмечаю, что это не одна операция, которая, в свою очередь, подразумевает, что вычислительные затраты на преобразование нескольких таких операций в атомарные намного дороже, что, в свою очередь, частично оправдывает, почему i++не является атомарной.
— kstratis 07

1
Хотя я понимаю вашу точку зрения, ваш ответ немного сбивает с толку. Я вижу пример и вывод, в котором говорится «из-за ситуации в примере»; imho, это неполное рассуждение :(
— Себастьян Мах

1
@phresnel Может быть, не самый педагогический ответ, но это лучшее, что я могу предложить на данный момент. Надеюсь, это поможет людям, а не запутает их. Однако спасибо за критику. Я постараюсь быть более точным в своих будущих сообщениях.
— kstratis

2

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


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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.