Почему я ++ не атомарный?

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

11 ответов

Решение

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 всегда были атомарными, они никогда не использовались бы, когда требовался неатомарный инк; программисты и компиляторы будут генерировать код, который загружает, добавляет 1 и сохраняет, потому что это будет намного быстрее.

В некоторых архитектурах набора команд нет атомарного inc или, возможно, нет inc совсем; чтобы сделать атомарный inc на MIPS, вы должны написать программный цикл, который использует ll а также sc: связанный с нагрузкой и условный для магазина. С привязкой к нагрузке считывает слово, а условное хранилище сохраняет новое значение, если слово не изменилось, или же оно терпит неудачу (что обнаруживается и вызывает повторную попытку).

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

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

Почему i++ не атомарен в Java?

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

Тема 1 и 2:

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

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

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

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

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

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

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

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

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

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

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

  1. вытащить меня из памяти
  2. установите я +1 на я

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

В Java операция i++ не является атомарной, поскольку на самом деле она представляет собой комбинацию нескольких шагов, которые могут быть прерваны другими потоками. Операция i++ состоит из трех отдельных этапов: чтение текущего значения i, увеличение значения на 1 и сохранение обновленного значения обратно в i. Каждый из этих шагов представляет собой отдельную операцию и может чередоваться с операциями других потоков, что может привести к возникновению условий гонки.

Состояние гонки возникает, когда несколько потоков одновременно обращаются к общему ресурсу, и результат операции зависит от конкретного порядка выполнения. В случае i++, если два или более потоков пытаются увеличить i одновременно, они могут прочитать одно и то же начальное значение i, увеличить его отдельно, а затем сохранить свои отдельные результаты обратно в i. Это может привести к потере обновлений, когда один или несколько приращений будут перезаписаны, что приведет к неправильному конечному значению i.

Чтобы обеспечить атомарность и избежать состояний гонки, Java предоставляет класс AtomicInteger, который инкапсулирует целочисленное значение и обеспечивает атомарные операции над этим значением. Вместо использования inti вы можете использовать AtomicInteger i и выполнять атомарные приращения с помощью метода приращенияAndGet(). Этот метод гарантирует, что операция приращения выполняется атомарно, и исключает возможность возникновения состояний гонки.

Вот пример, демонстрирующий атомарное приращение с использованием AtomicInteger:

      import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Test {

    private static AtomicInteger total = new AtomicInteger(0);
    private static AtomicInteger countT1 = new AtomicInteger(0);
    private static AtomicInteger countT2 = new AtomicInteger(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.get() + countT2.get() + " == " + total.get()));
    }

    private Runnable t1 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total.incrementAndGet();
                countT1.incrementAndGet();
                System.out.println("Hello #" + countT1.get() + " from Thread 1! Total hello: " + total.get());
            }
        }
    };

    private Runnable t2 = new Runnable() {
        @Override
        public void run() {
            while (run) {
                total.incrementAndGet();
                countT2.incrementAndGet();
                System.out.println("Hello #" + countT2.get() + " from Thread 2! Total hello: " + total.get());
            }
        }
    };

    public static void main(String[] args) {
        new Test();
    }
}

В JVM или любой виртуальной машине i++ эквивалентно следующему:

      int temp = i;     // 1. read
i = temp + 1;    // 2. increment the value then 3. write it back

вот почему i++ не атомарен.

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

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

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

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

Другие вопросы по тегам