Dlaczego i ++ nie jest atomowy?

97

Dlaczego i++atomic nie jest w Javie?

Aby zagłębić się w Javę, spróbowałem policzyć, jak często wykonywane są pętle w wątkach.

Więc użyłem

private static int total = 0;

w klasie głównej.

Mam dwa wątki.

  • Wątek 1: Wydruki System.out.println("Hello from Thread 1!");
  • Wątek 2: Wydruki System.out.println("Hello from Thread 2!");

Liczę wiersze wydrukowane przez wątek 1 i wątek 2. Ale wiersze wątku 1 + wiersze wątku 2 nie pasują do całkowitej liczby wydrukowanych wierszy.

Oto mój kod:

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
źródło
14
Dlaczego nie spróbujesz AtomicInteger?
Braj
3
maszyna JVM ma iincoperację zwiększania liczb całkowitych, ale działa to tylko w przypadku zmiennych lokalnych, w przypadku których współbieżność nie ma znaczenia. W przypadku pól kompilator osobno generuje polecenia odczytu, modyfikacji i zapisu.
Silly Freak
14
Dlaczego w ogóle miałbyś oczekiwać, że będzie atomowy?
Hot Licks
2
@Silly Freak: nawet jeśli nie było iincinstrukcji dla pól, o pojedynczej instrukcji nie gwarantuje niepodzielność, np nieprzestrzegania volatile longoraz doubledostęp do pola w nie gwarancją atomowych, niezależnie od faktu, że jest ona wykonywana przez jednego instrukcji kodu bajtowego.
Holger,

Odpowiedzi:

125

i++ prawdopodobnie nie jest atomowy w Javie, ponieważ atomowość jest specjalnym wymaganiem, którego nie ma w większości zastosowań i++ . To wymaganie wiąże się ze znacznymi kosztami ogólnymi: wykonanie atomowej operacji przyrostowej wiąże się z dużym kosztem; obejmuje synchronizację zarówno na poziomie oprogramowania, jak i sprzętu, które nie muszą występować w zwykłych przyrostach.

Możesz przedstawić argument, który i++powinien zostać zaprojektowany i udokumentowany jako konkretnie wykonujący przyrost atomowy, tak aby przyrost nieatomowy był wykonywany przy użyciu i = i + 1. Jednak spowodowałoby to zerwanie „kulturowej zgodności” między Javą, C i C ++. Poza tym zabrałoby to wygodną notację, którą programiści znający języki podobne do C przyjmują za pewnik, nadając jej specjalne znaczenie, które ma zastosowanie tylko w ograniczonych okolicznościach.

Podstawowy kod C lub C ++, taki jak for (i = 0; i < LIMIT; i++)można przetłumaczyć na Javę jako for (i = 0; i < LIMIT; i = i + 1); ponieważ użycie atomu byłoby niewłaściwe i++. Co gorsza, programiści przechodzący z C lub innych języków podobnych do języka C na Javę i i++tak użyliby, co spowodowałoby niepotrzebne użycie instrukcji atomowych.

Nawet na poziomie zestawu instrukcji maszynowych operacja typu przyrostowego zwykle nie jest atomowa ze względu na wydajność. W x86, aby uczynić incinstrukcję atomową, należy użyć specjalnego „przedrostka blokady” : z tych samych powodów, co powyżej. Gdyby inczawsze był atomowy, nigdy nie byłby używany, gdy wymagany jest nieatomowy inc; programiści i kompilatorzy wygenerowaliby kod, który ładuje, dodaje 1 i przechowuje, ponieważ byłoby to znacznie szybsze.

W niektórych architekturach zestawu instrukcji nie ma atomów inclub być może wcale inc; aby zrobić atomic inc na MIPS, musisz napisać pętlę oprogramowania, która używa lland sc: load-linked i store-conditional. Load-linked odczytuje słowo, a warunek przechowywania przechowuje nową wartość, jeśli słowo się nie zmieniło, albo nie powiedzie się (co jest wykrywane i powoduje ponowną próbę).

Kaz
źródło
2
ponieważ java nie ma wskaźników, inkrementacja zmiennych lokalnych jest z natury rzeczy zapisywana w wątku, więc w przypadku pętli problem nie byłby taki zły. Twój punkt widzenia na temat najmniejszego zaskoczenia jest oczywiście ważny. również, jak to jest, i = i + 1byłoby tłumaczeniem dla ++i, a niei++
Silly Freak
22
Pierwsze słowo pytania brzmi „dlaczego”. Jak na razie jest to jedyna odpowiedź na pytanie „dlaczego”. Inne odpowiedzi tak naprawdę tylko ponownie formułują pytanie. Więc +1.
Dawood ibn Kareem
3
Warto zauważyć, że gwarancja atomowości nie rozwiązałaby problemu widoczności aktualizacji volatilepól niebędących polami. Więc jeśli nie potraktujesz każdego pola jako niejawnie, volatilegdy jeden wątek użyje na nim ++operatora, taka atomowa gwarancja nie rozwiązałaby problemów z jednoczesną aktualizacją. Po co więc marnować wydajność na coś, jeśli to nie rozwiązuje problemu.
Holger,
1
@DavidWallace nie masz na myśli ++? ;)
Dan Hlavenka
36

i++ obejmuje dwie operacje:

  1. przeczytaj aktualną wartość i
  2. zwiększ wartość i przypisz ją do i

Gdy dwa wątki działają i++na tej samej zmiennej w tym samym czasie, mogą uzyskać tę samą bieżącą wartość i, a następnie ją zwiększać i ustawiać i+1, aby uzyskać pojedynczy przyrost zamiast dwóch.

Przykład:

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
Eran
źródło
(Nawet gdyby i++ był atomowy, nie byłoby dobrze zdefiniowane / bezpieczne dla wątków.)
user2864740
15
+1, ale „1. A, 2. B i C” brzmi jak trzy operacje, a nie dwie. :)
yshavit
3
Należy zauważyć, że nawet jeśli operacja została zaimplementowana za pomocą pojedynczej instrukcji maszynowej, która zwiększyłaby miejsce przechowywania, nie ma gwarancji, że będzie bezpieczna wątkowo. Maszyna nadal musi pobrać wartość, zwiększyć ją i zapisać z powrotem, a ponadto może istnieć wiele kopii pamięci podręcznej tego miejsca przechowywania.
Hot Licks
3
@Aquarelle - Jeśli dwa procesory wykonują tę samą operację w tym samym miejscu przechowywania jednocześnie i nie ma transmisji „rezerwowej” w tej lokalizacji, to prawie na pewno będą zakłócać i dawać fałszywe wyniki. Tak, operacja ta może być „bezpieczna”, ale wymaga specjalnego wysiłku, nawet na poziomie sprzętowym.
Hot Licks
6
Ale myślę, że pytanie brzmiało „Dlaczego”, a nie „Co się dzieje”.
Sebastian Mach
11

Ważną rzeczą jest raczej JLS (specyfikacja języka Java), a nie to, jak różne implementacje JVM mogą, ale nie muszą, zaimplementować określoną cechę języka. JLS definiuje operator postfiksu ++ w klauzuli 15.14.2, który mówi m.in., że „wartość 1 jest dodawana do wartości zmiennej, a suma jest ponownie zapisywana w zmiennej”. Nigdzie nie wspomina ani nie sugeruje wielowątkowości lub atomowości. W tym przypadku JLS zapewnia zmienne i zsynchronizowane . Dodatkowo istnieje pakiet java.util.concurrent.atomic (patrz http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )

Jonathan Rosenne
źródło
5

Dlaczego i ++ nie jest atomowy w Javie?

Podzielmy operację inkrementacji na wiele instrukcji:

Wątek 1 i 2:

  1. Pobierz całkowitą wartość z pamięci
  2. Dodaj 1 do wartości
  3. Napisz z powrotem do pamięci

Jeśli nie ma synchronizacji, powiedzmy, że Wątek odczytał wartość 3 i zwiększył ją do 4, ale nie zapisał jej z powrotem. W tym momencie następuje zmiana kontekstu. Wątek drugi odczytuje wartość 3, zwiększa ją i następuje przełączenie kontekstu. Chociaż oba wątki zwiększyły całkowitą wartość, nadal będzie to 4 - stan wyścigu.

Aniket Thakur
źródło
2
Nie rozumiem, jak powinna to być odpowiedź na pytanie. Język może zdefiniować każdą cechę jako atomową, czy to przyrosty, czy jednorożce. Po prostu dajesz przykład konsekwencji braku bycia atomowym.
Sebastian Mach
Tak, język może zdefiniować dowolną funkcję jako atomową, ale o ile java jest uważana za operator inkrementacji (co jest pytaniem opublikowanym przez OP), nie jest atomowa, a moja odpowiedź podaje przyczyny.
Aniket Thakur
1
(przepraszam za mój ostry ton w pierwszym komentarzu) Ale z drugiej strony powodem wydaje się być „ponieważ gdyby był atomowy, to nie byłoby warunków wyścigu”. To znaczy, brzmi to tak, jakby stan wyścigu był pożądany.
Sebastian Mach
@phresnel, narzut wprowadzony w celu utrzymania przyrostu atomowego jest ogromny i rzadko pożądany, dzięki czemu operacja jest tania, w wyniku czego przez większość czasu pożądana jest metoda nieatomowa.
josefx
4
@josefx: Zauważ, że nie kwestionuję faktów, ale rozumowanie w tej odpowiedzi. Zasadniczo mówi, że „i ++ nie jest atomowy w Javie ze względu na warunki wyścigu” , co jest jak powiedzenie „samochód nie ma poduszki powietrznej ze względu na zderzenia, które mogą się zdarzyć” lub „nie dostaniesz noża z zamówieniem currywurst, ponieważ wurst może wymagać wycięcia ” . Dlatego nie sądzę, żeby to była odpowiedź. Pytanie nie brzmiało „Co robi i ++?” lub „Jakie są konsekwencje braku synchronizacji i ++?” .
Sebastian Mach
5

i++ to instrukcja, która składa się po prostu z 3 operacji:

  1. Odczytaj aktualną wartość
  2. Wpisz nową wartość
  3. Przechowuj nową wartość

Te trzy operacje nie mają być wykonywane w jednym kroku lub innymi słowy i++nie są operacjami złożonymi . W rezultacie różne rzeczy mogą się nie udać, gdy więcej niż jeden wątek jest zaangażowany w pojedynczą, ale niezłożoną operację.

Rozważ następujący scenariusz:

Czas 1 :

Thread A fetches i
Thread B fetches i

Czas 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)

Czas 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 masz to. Stan wyścigu.


Dlatego i++nie jest atomowy. Gdyby tak było, nic z tego by się nie wydarzyło i każde z nich fetch-update-storewydarzyłoby się atomowo. Właśnie do tego AtomicIntegersłuży iw twoim przypadku prawdopodobnie by pasował.

PS

Doskonała książka obejmująca wszystkie te kwestie, a także kilka z nich to: Współbieżność języka Java w praktyce

kstratis
źródło
1
Hmm. Język może zdefiniować każdą cechę jako atomową, czy to przyrosty, czy jednorożce. Po prostu dajesz przykład konsekwencji braku bycia atomowym.
Sebastian Mach
@phresnel Dokładnie. Ale zwracam również uwagę, że nie jest to pojedyncza operacja, co w konsekwencji oznacza, że ​​koszt obliczeniowy przekształcenia wielu takich operacji w operacje atomowe jest znacznie droższy, co z kolei - częściowo - uzasadnia, dlaczego i++nie jest atomowy.
kstratis
1
Chociaż rozumiem twój punkt widzenia, twoja odpowiedź jest nieco myląca z nauką. Widzę przykład i wniosek, który mówi „ze względu na sytuację w przykładzie”; imho, to jest niepełne rozumowanie :(
Sebastian Mach
1
@phresnel Może nie jest to najbardziej pedagogiczna odpowiedź, ale jest to najlepsza, jaką mogę obecnie zaoferować. Miejmy nadzieję, że pomoże to ludziom i ich nie zmyli. Jednak dzięki za krytykę. Postaram się być bardziej precyzyjny w przyszłych postach.
kstratis
2

W JVM przyrost obejmuje odczyt i zapis, więc nie jest atomowy.

celeritas
źródło
2

Gdyby operacja i++była atomowa, nie miałbyś szansy odczytać z niej wartości. To jest dokładnie to, co chcesz zrobić używając i++(zamiast używać ++i).

Na przykład spójrz na następujący kod:

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

W tym przypadku oczekujemy, że wyjście będzie: 0 (ponieważ wysyłamy inkrementację, np. Najpierw czytamy, potem aktualizujemy)

Jest to jeden z powodów, dla których operacja nie może być atomowa, ponieważ musisz odczytać wartość (i coś z nią zrobić), a następnie zaktualizować wartość.

Innym ważnym powodem jest to, że zrobienie czegoś atomowo zwykle zajmuje więcej czasu z powodu blokowania. Byłoby głupie, gdyby wszystkie operacje na prymitywach trwały trochę dłużej w rzadkich przypadkach, gdy ludzie chcą mieć operacje atomowe. Dlatego dodali do języka AtomicIntegeri inne klasy atomowe.

Roy van Rijn
źródło
2
To jest mylące. Musisz oddzielić wykonanie od uzyskania wyniku, w przeciwnym razie nie możesz uzyskać wartości z żadnej operacji atomowej.
Sebastian Mach
Nie, nie jest, dlatego AtomicInteger w Javie ma metodę get (), getAndIncrement (), getAndDecrement (), inkrementAndGet (), decrementAndGet () itd.
Roy van Rijn
1
A język Java można było zdefiniować i++jako rozszerzenie i.getAndIncrement(). Taka ekspansja nie jest nowa. Np. Lambdy w C ++ są rozszerzane do anonimowych definicji klas w C ++.
Sebastian Mach
Biorąc pod uwagę atomowy, i++można w trywialny sposób stworzyć atomowy ++ilub odwrotnie. Jeden jest równoważny drugiemu plus jeden.
David Schwartz
2

Istnieją dwa kroki:

  1. ściągnij mnie z pamięci
  2. ustaw i + 1 na i

więc nie jest to operacja atomowa. Gdy wątek1 wykonuje i ++, a wątek2 wykonuje i ++, końcową wartością i może być i + 1.

yanghaogn
źródło
-1

Współbieżność ( Threadklasa i inne) to dodatkowa funkcja w wersji 1.0 języka Java . i++został dodany wcześniej w wersji beta i jako taki jest nadal bardziej niż prawdopodobne w swojej (mniej lub bardziej) oryginalnej implementacji.

Synchronizacja zmiennych należy do programisty. Sprawdź samouczek Oracle na ten temat .

Edycja: aby wyjaśnić, i ++ jest dobrze zdefiniowaną procedurą, która poprzedza Javę, i jako taka projektanci Javy zdecydowali się zachować oryginalną funkcjonalność tej procedury.

Operator ++ został zdefiniowany w B (1969), który jest tylko odrobinę starszy od języka Java i obsługi wątków.

Nietoperz
źródło
-1 "publiczna klasa Wątek ... Od: JDK1.0" Źródło: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak
Wersja nie ma tak dużego znaczenia, jak fakt, że była jeszcze zaimplementowana przed klasą Thread i nie została przez to zmieniona, ale zredagowałem swoją odpowiedź, aby cię zadowolić.
TheBat
5
Liczy się to, że twoje twierdzenie, że „zostało jeszcze zaimplementowane przed klasą Thread” nie jest poparte przez źródła. i++brak bycia atomowym to decyzja projektowa, a nie przeoczenie w rozwijającym się systemie.
Silly Freak
Lol, to słodkie. i ++ zostało zdefiniowane na długo przed Threads, po prostu dlatego, że istniały języki, które istniały przed Javą. Twórcy Javy wykorzystali te inne języki jako podstawę zamiast redefiniowania dobrze przyjętej procedury. Gdzie ja kiedykolwiek powiedziałem, że to przeoczenie?
TheBat
@SillyFreak Oto kilka źródeł, które pokazują, ile lat ma ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat