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

Почему 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.