Чому i ++ не є атомним?


97

Чому i++в Java не є атомним?

Щоб трохи глибше зрозуміти Java, я спробував підрахувати, як часто виконується цикл у потоках.

Тому я використав a

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?
Брадж


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 ++ як for (i = 0; i < LIMIT; i++)би перекладається на Java як for (i = 0; i < LIMIT; i = i + 1); бо було б недоцільно використовувати атомний i++. Гірше того, що програмісти, що надходять із мови C або інших мов, подібних до мови C, i++все одно використовують , що призведе до непотрібного використання атомних інструкцій.

Навіть на рівні набору машинних інструкцій операція типу збільшення зазвичай не є атомарною з міркувань продуктивності. У x86 для використання incатомарної інструкції повинна використовуватися спеціальна інструкція "префікс блокування" : з тих самих причин, що і вище. Якби incвони завжди були атомними, їх ніколи б не використовували, коли потрібен неатомний розряд; програмісти та компілятори генеруватимуть код, який завантажується, додає 1 і зберігає, оскільки це буде набагато швидше.

У деяких архітектурах набору інструкцій атомних incабо, можливо, взагалі немає inc; щоб зробити атомний розряд на MIPS, вам потрібно написати програмний цикл, який використовує llі sc: зв'язане з навантаженням, і умовне зберігання. Load-linked читає слово, а умовно зберігає нове значення, якщо слово не змінилося, або воно не вдається (що виявляється та викликає повторну спробу).


2
оскільки Java не має покажчиків, збільшення локальних змінних за своєю суттю є збереженням потоку, тому з циклами проблема в основному не буде настільки поганою. Ваша думка щодо найменших сюрпризів, звичайно. також, як є, i = i + 1це був би переклад для ++i, а неi++
Silly Freak

22
Перше слово питання - "чому". Наразі це єдина відповідь для вирішення питання "чому". Інші відповіді насправді просто повторно формулюють питання. Отже +1.
Dawood ibn Kareem

3
Варто зауважити, що гарантія атомності не вирішить проблему видимості оновлень неполів volatile. Отже, якщо ви не будете поводитися з кожним полем неявно, як volatileтільки один потік використовує ++оператор на ньому, така гарантія атомності не вирішить одночасні проблеми оновлення. То навіщо потенційно витрачати продуктивність на щось, якщо це не вирішує проблему.
Holger

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++ був атомним, це не було б чітко визначеною / безпечною для потоків поведінкою.)
user2864740

15
+1, але "1. A, 2. B і C" звучить як три операції, а не дві. :)
yshavit

3
Зауважте, що навіть якби операція була здійснена за допомогою однієї машинної інструкції, яка збільшила місце зберігання на місці, немає гарантії, що це буде безпечно для потоків. Машині все ще потрібно отримати значення, збільшити його і зберегти назад, плюс може бути кілька копій кеша цього місця зберігання.
Гарячий лизає

3
@Aquarelle - Якщо два процесори виконують одну і ту ж операцію над одним і тим же місцем зберігання одночасно, і немає "резервного" мовлення в цьому місці, то вони майже напевно будуть втручатися і даватимуть помилкові результати. Так, ці операції можуть бути "безпечними", але це вимагає особливих зусиль, навіть на апаратному рівні.
Гарячий лизає

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. Перепишіть у пам’ять

Якщо синхронізації немає, то скажімо, що "Thread one" прочитав значення 3 і збільшив його до 4, але не записав його назад. На цьому етапі відбувається перемикання контексту. Потік два читає значення 3, збільшує його, і відбувається перемикання контексту. Незважаючи на те, що обидва потоки збільшили загальне значення, воно все одно буде 4 - расовим станом.


2
Я не розумію, як це має бути відповіддю на питання. Мова може визначити будь-яку ознаку як атомну, будь то інкременти або єдинороги. Ви просто ілюструєте наслідок того, що ви не атомні.
Себастьян Мах

Так, мова може визначити будь-яку функцію як атомну, але оскільки java вважається оператором приросту (саме це питання розміщує OP) не є атомною, і моя відповідь вказує причини.
Aniket Thakur

1
(вибачте за мій різкий тон у першому коментарі) Але тоді, здається, причина в тому, "оскільки якби це було атомно, то не було б умов перегонів". Тобто це звучить так, ніби бажані умови перегонів.
Себастьян Мах

@phresnel накладні витрати, введені для збереження приросту атома, величезні і рідко бажані, завдяки чому операція дешева, і як результат неатомна бажана більшість часу.
josefx

4
@josefx: Зауважте, що я не ставлю під сумнів факти, а міркування у цій відповіді. Це, по суті, говорить: "i ++ не є атомним на Яві через перегони, які він має" , це як би сказати "автомобіль не має подушки безпеки через аварії, які можуть статися" або "ви не отримаєте ножа з вашим замовленням currywurst, тому що може бути потрібно скоротити вурст " . Таким чином, я не думаю, що це відповідь. Питання не було "Що робить 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 на практиці


1
Хм Мова може визначити будь-яку ознаку як атомну, будь то інкременти або єдинороги. Ви просто ілюструєте наслідок того, що ви не атомні.
Себастьян Мах

@phresnel Точно. Але я також зазначаю, що це не одна операція, яка, в свою чергу, означає, що обчислювальні витрати на перетворення декількох таких операцій в атомні є набагато дорожчими, що, в свою чергу, частково, виправдовує, чому i++не є атомною.
kstratis

1
Поки я розумію вашу думку, ваша відповідь трохи заплутала навчання. Я бачу приклад і висновок, який говорить "через ситуацію на прикладі"; imho це неповне міркування :(
Себастьян Мах

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


2

Якщо операція i++буде атомною, ви не зможете прочитати з неї значення. Це саме те, що ви хочете зробити, використовуючи i++(а не використовуючи ++i).

Наприклад, подивіться на наступний код:

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

У цьому випадку ми очікуємо, що результат буде: 0 (оскільки ми публікуємо приріст, наприклад, спочатку читаємо, а потім оновлюємо)

Це одна з причин, чому операція не може бути атомарною, оскільки вам потрібно прочитати значення (і щось з ним зробити), а потім оновити значення.

Інша важлива причина полягає в тому, що виконання атомарних дій зазвичай займає більше часу через блокування. Було б безглуздо, якби всі операції з примітивами займали трохи більше часу в тих рідкісних випадках, коли люди хочуть мати атомні операції. Ось чому вони додали до мови AtomicIntegerта інші атомні класи.


2
Це вводить в оману. Ви повинні відокремити виконання та отримання результату, інакше ви не зможете отримати значення від будь-якої атомної операції.
Себастьян Мах

Ні, це не так, тому AtomicInteger Java має get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () тощо
Рой ван Рейн,

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/…
Silly Freak

Версія не так важлива, як той факт, що вона все ще була реалізована до класу Thread і не була змінена через неї, але я відредагував свою відповідь, щоб сподобатися вам.
TheBat

5
Важливо те, що ваша претензія "вона все ще була реалізована до класу Thread" не підкріплена джерелами. i++не бути атомними - це дизайнерське рішення, а не нагляд у зростаючій системі.
Silly Freak

Лол, це мило. i ++ був визначений задовго до Threads, просто тому, що існували мови, які існували до Java. Творці Java використовували ці інші мови як основу, замість того, щоб переосмислити добре прийняту процедуру. Де я коли-небудь казав, що це недогляд?
TheBat

@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.