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();
}
}
java
multithreading
concurrency
Andie2302
źródło
źródło
AtomicInteger
?iinc
operację 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.iinc
instrukcji dla pól, o pojedynczej instrukcji nie gwarantuje niepodzielność, np nieprzestrzeganiavolatile
long
orazdouble
dostęp do pola w nie gwarancją atomowych, niezależnie od faktu, że jest ona wykonywana przez jednego instrukcji kodu bajtowego.Odpowiedzi:
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życiui = 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ę jakofor (i = 0; i < LIMIT; i = i + 1)
; ponieważ użycie atomu byłoby niewłaściwei++
. Co gorsza, programiści przechodzący z C lub innych języków podobnych do języka C na Javę ii++
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ć
inc
instrukcję atomową, należy użyć specjalnego „przedrostka blokady” : z tych samych powodów, co powyżej. Gdybyinc
zawsze 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
inc
lub być może wcaleinc
; aby zrobić atomic inc na MIPS, musisz napisać pętlę oprogramowania, która używall
andsc
: 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ę).źródło
i = i + 1
byłoby tłumaczeniem dla++i
, a niei++
volatile
pól niebędących polami. Więc jeśli nie potraktujesz każdego pola jako niejawnie,volatile
gdy 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.++
? ;)i++
obejmuje dwie operacje:i
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:
źródło
i++
był atomowy, nie byłoby dobrze zdefiniowane / bezpieczne dla wątków.)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 )
źródło
Podzielmy operację inkrementacji na wiele instrukcji:
Wątek 1 i 2:
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.
źródło
i++
to instrukcja, która składa się po prostu z 3 operacji: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 :
Czas 2 :
Czas 3 :
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 nichfetch-update-store
wydarzyłoby się atomowo. Właśnie do tegoAtomicInteger
sł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
źródło
i++
nie jest atomowy.W JVM przyrost obejmuje odczyt i zapis, więc nie jest atomowy.
źródło
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ąci++
(zamiast używać++i
).Na przykład spójrz na następujący kod:
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
AtomicInteger
i inne klasy atomowe.źródło
i++
jako rozszerzeniei.getAndIncrement()
. Taka ekspansja nie jest nowa. Np. Lambdy w C ++ są rozszerzane do anonimowych definicji klas w C ++.i++
można w trywialny sposób stworzyć atomowy++i
lub odwrotnie. Jeden jest równoważny drugiemu plus jeden.Istnieją dwa kroki:
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.
źródło
Współbieżność (
Thread
klasa 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.
źródło
i++
brak bycia atomowym to decyzja projektowa, a nie przeoczenie w rozwijającym się systemie.