JavaRush /Blog Java /Random-PL /Nie możesz zepsuć Javy wątkiem: część III - interakcja
Viacheslav
Poziom 3

Nie możesz zepsuć Javy wątkiem: część III - interakcja

Opublikowano w grupie Random-PL
Krótki przegląd funkcji interakcji wątków. Wcześniej przyglądaliśmy się, jak wątki synchronizują się ze sobą. Tym razem zagłębimy się w problemy, które mogą pojawić się podczas interakcji wątków i porozmawiamy o tym, jak można ich uniknąć. Podamy również kilka przydatnych linków do głębszych badań. Nie można zrujnować Javy wątkiem: Część III – interakcja – 1

Wstęp

Wiemy zatem, że w Javie istnieją wątki, o których przeczytacie w recenzji „ Wątek nie zepsuje Javy: Część I - Wątki ” i że wątki można ze sobą synchronizować, o czym pisaliśmy w recenzji „ Wątek nie może zepsuć Java ” Spoil: Część II - Synchronizacja ”. Czas porozmawiać o tym, jak wątki oddziałują na siebie. W jaki sposób dzielą wspólne zasoby? Jakie mogą być z tym związane problemy?

Impas

Najgorszym problemem jest Deadlock. Gdy dwa lub więcej wątków czeka na siebie w nieskończoność, nazywa się to zakleszczeniem. Weźmy przykład ze strony Oracle z opisu koncepcji „ Deadlock ”:
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
Zakleszczenie może nie pojawić się za pierwszym razem, ale jeśli wykonanie programu utknęło w miejscu, czas uruchomić jvisualvm: Nie można zrujnować Javy wątkiem: Część III – interakcja – 2Jeśli wtyczka jest zainstalowana w JVisualVM (poprzez Narzędzia -> Wtyczki), możemy zobaczyć, gdzie wystąpił zakleszczenie:
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Wątek 1 oczekuje na blokadę z wątku 0. Dlaczego tak się dzieje? Thread-1rozpoczyna wykonywanie i wykonuje metodę Friend#bow. Jest ona oznaczona słowem-kluczem synchronized, czyli monitor odbieramy po this. Na wejściu do metody otrzymaliśmy link do kolejnego Friend. Teraz wątek Thread-1chce wykonać metodę na innym Friend, uzyskując w ten sposób również od niego blokadę. Jeśli jednak inny wątek (w tym przypadku Thread-0) zdołał wejść do metody bow, to zamek jest już zajęty i Thread-1czeka Thread-0i odwrotnie. Blokada jest nierozwiązywalna, więc jest martwa, to znaczy martwa. Zarówno uścisk śmierci (którego nie można zwolnić), jak i martwy blok, z którego nie można uciec. Na temat zakleszczenia możesz obejrzeć film: „ Zakleszczenie – współbieżność nr 1 – zaawansowana Java ”.

Blokada życia

Jeśli istnieje zakleszczenie, to czy istnieje blokada na żywo? Tak, jest) Livelock polega na tym, że wątki wydają się żyć na zewnątrz, ale jednocześnie nie mogą nic zrobić, ponieważ ... warunek, pod którym starają się kontynuować swoją pracę, nie może zostać spełniony. W istocie Livelock jest podobny do impasu, ale wątki nie „zawieszają się” w systemie w oczekiwaniu na monitor, ale zawsze coś robią. Na przykład:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";

    public static void log(String text) {
        String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
Powodzenie tego kodu zależy od kolejności, w jakiej program planujący wątki Java uruchamia wątki. Jeśli zacznie się jako pierwszy Thead-1, otrzymamy Livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Jak widać na przykładzie, oba wątki na zmianę próbują przechwycić obie blokady, ale kończy się to niepowodzeniem. Co więcej, nie są w impasie, to znaczy wizualnie wszystko jest z nimi w porządku i wykonują swoją pracę. Nie można zrujnować Javy wątkiem: Część III – interakcja – 3Według JVisualVM widzimy okresy uśpienia i okres parkowania (w tym momencie wątek próbuje zająć blokadę, przechodzi w stan parkowania, co omawialiśmy wcześniej, mówiąc o synchronizacji wątków ). Jeśli chodzi o livelock, możesz zobaczyć przykład: „ Java - Thread Livelock ”.

Głód

Oprócz blokowania (zakleszczenie i blokada na żywo) podczas pracy z wielowątkowością pojawia się inny problem - głód lub „głód”. Zjawisko to różni się od blokowania tym, że wątki nie są blokowane, ale po prostu nie mają wystarczających zasobów dla wszystkich. Dlatego chociaż niektóre wątki przejmują cały czas wykonywania, inne nie mogą zostać wykonane: Nie można zrujnować Javy wątkiem: Część III – interakcja – 4

https://www.logicbig.com/

Świetny przykład można znaleźć tutaj: „ Java - Głód i uczciwość wątków ”. Ten przykład pokazuje, jak wątki działają w Starvation i jak jedna mała zmiana z Thread.sleep na Thread.wait może równomiernie rozłożyć obciążenie. Nie można zrujnować Javy wątkiem: Część III – interakcja – 5

Warunki wyścigu

Podczas pracy z wielowątkowością istnieje coś takiego jak „warunek wyścigu”. Zjawisko to polega na tym, że wątki współdzielą między sobą pewien zasób, a kod jest napisany w taki sposób, że nie zapewnia w tym przypadku poprawnego działania. Spójrzmy na przykład:
public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
Ten kod może nie wygenerować błędu za pierwszym razem. A może to wyglądać tak:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
    at App.lambda$main$0(App.java:13)
    at java.lang.Thread.run(Thread.java:745)
Jak widać w trakcie przypisywania newValuecoś poszło nie tak i newValuebyło ich więcej. Niektóre wątki w stanie wyścigu udało się zmienić valuemiędzy tymi dwoma zespołami. Jak widzimy pojawił się wyścig pomiędzy wątkami. A teraz wyobraź sobie, jak ważne jest, aby nie popełniać podobnych błędów przy transakcjach pieniężnych... Przykłady i diagramy można zobaczyć także tutaj: „ Kod symulujący sytuację wyścigową w wątku Java ”.

Lotny

Mówiąc o współdziałaniu wątków, warto szczególnie zwrócić uwagę na słowo kluczowe volatile. Spójrzmy na prosty przykład:
public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
Najciekawsze jest to, że z dużym prawdopodobieństwem to nie zadziała. Nowy wątek nie zobaczy zmian flag. Aby to naprawić, flagmusisz określić słowo kluczowe dla pola volatile. Jak i dlaczego? Wszystkie działania są wykonywane przez procesor. Ale wyniki obliczeń trzeba gdzieś przechowywać. W tym celu na procesorze znajduje się pamięć główna i sprzętowa pamięć podręczna. Te pamięci podręczne procesorów przypominają mały fragment pamięci umożliwiający dostęp do danych szybciej niż dostęp do pamięci głównej. Ale wszystko ma też swoją wadę: dane w pamięci podręcznej mogą być nieaktualne (jak w powyższym przykładzie, gdy wartość flagi nie została zaktualizowana). Zatem słowo kluczowe volatilemówi maszynie JVM, że nie chcemy buforować naszej zmiennej. Dzięki temu możesz zobaczyć rzeczywisty wynik we wszystkich wątkach. Jest to bardzo uproszczone sformułowanie. Na ten temat volatilezdecydowanie zaleca się przeczytanie tłumaczenia „ JSR 133 (Java Memory Model) FAQ ”. Radzę także przeczytać więcej o materiałach „ Model pamięci Java ” i „ Java Volatile Keyword ”. Poza tym trzeba pamiętać, że volatilechodzi tu o widoczność, a nie o atomowość zmian. Jeśli weźmiemy kod z „Race Condition”, zobaczymy podpowiedź w IntelliJ Idea: Nie można zrujnować Javy wątkiem: Część III – interakcja – 6Ta inspekcja (Inspekcja) została dodana do IntelliJ Idea jako część problemu IDEA-61117 , który został wymieniony w uwagach do wydania w 2010 roku.

Atomowość

Operacje atomowe to operacje, których nie można podzielić. Na przykład operacja przypisania wartości do zmiennej jest niepodzielna. Niestety przyrost nie jest operacją atomową, ponieważ przyrost wymaga aż trzech operacji: pobierz starą wartość, dodaj ją do niej i zapisz wartość. Dlaczego atomowość jest ważna? W przykładzie przyrostu, jeśli wystąpi sytuacja wyścigu, w dowolnym momencie współdzielony zasób (tj. współdzielona wartość) może nagle się zmienić. Ponadto ważne jest, aby struktury 64-bitowe również nie były atomowe, np. longi double. Więcej możesz przeczytać tutaj: „ Zapewnij atomowość podczas odczytu i zapisu wartości 64-bitowych ”. Przykład problemów z atomowością można zobaczyć w następującym przykładzie:
public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
Specjalna klasa do pracy z atomem Integerzawsze pokaże nam 30000, ale valueod czasu do czasu będzie się to zmieniać. Istnieje krótki przegląd tego tematu „ Wprowadzenie do zmiennych atomowych w Javie ”. Atomic opiera się na algorytmie Porównaj i Zamień. Więcej na ten temat przeczytacie w artykule na temat Habrégo „ Porównanie algorytmów Lock-free – CAS i FAA na przykładzie JDK 7 i 8 ” lub na Wikipedii w artykule o „ Porównaniu z wymianą ”. Nie można zepsuć Javy wątkiem: Część III - interakcja - 8

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Dzieje się wcześniej

Jest ciekawa i tajemnicza rzecz – zdarza się wcześniej. Skoro już mowa o przepływach, warto o tym także poczytać. Relacja Dzieje się przed wskazuje kolejność, w jakiej będą widoczne akcje między wątkami. Interpretacji i interpretacji jest wiele. Jednym z najnowszych raportów na ten temat jest ten raport:
Chyba lepiej, że ten film nic na ten temat nie mówi. Zostawię więc tylko link do filmu. Możesz przeczytać „ Java – Zrozumienie relacji przed relacjami ”.

Wyniki

W tej recenzji przyjrzeliśmy się funkcjom interakcji wątków. Omówiliśmy problemy, które mogą się pojawić oraz sposoby ich wykrywania i eliminowania. Lista dodatkowych materiałów na ten temat: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION