CodeGym/Blog Java/Random-PL/Razem lepiej: Java i klasa Thread. Część III — Interakcja...
John Squirrels
Poziom 41
San Francisco

Razem lepiej: Java i klasa Thread. Część III — Interakcja

Opublikowano w grupie Random-PL
Krótki przegląd szczegółowych informacji na temat interakcji wątków. Wcześniej przyjrzeliśmy się, w jaki sposób wątki są ze sobą synchronizowane. Tym razem zagłębimy się w problemy, które mogą pojawić się podczas interakcji wątków i porozmawiamy o tym, jak ich uniknąć. Podamy również kilka przydatnych linków do bardziej dogłębnej analizy. Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 1

Wstęp

Wiemy więc, że Java ma wątki. Można o tym przeczytać w recenzji zatytułowanej Better together: Java and the Thread class. Część I — Wątki egzekucyjne . Fakt, że wątki mogą się ze sobą synchronizować, zbadaliśmy w recenzji zatytułowanej Better together: Java and the Thread class. Część II — Synchronizacja . Nadszedł czas, aby porozmawiać o tym, jak wątki wchodzą ze sobą w interakcje. W jaki sposób udostępniają wspólne zasoby? Jakie problemy mogą się tu pojawić? Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 2

Impas

Najbardziej przerażającym problemem jest zakleszczenie. Zakleszczenie ma miejsce, gdy dwa lub więcej wątków wiecznie czeka na drugi. Weźmy przykład ze strony internetowej Oracle, który opisuje zakleszczenie :
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 bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s 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 wystąpić tutaj za pierwszym razem, ale jeśli twój program się zawiesi, czas go uruchomić jvisualvm: Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 3Po zainstalowaniu wtyczki 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 <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Wątek 1 czeka na blokadę z wątku 0. Dlaczego tak się dzieje? Thread-1zaczyna działać i wykonuje Friend#bowmetodę. Jest oznaczony słowem synchronizedkluczowym, co oznacza, że ​​pozyskujemy monitor dla this(aktualnego obiektu). Wejście metody było odniesieniem do innego Friendobiektu. Teraz Thread-1chce wykonać metodę na innym Friendi musi uzyskać jego blokadę, aby to zrobić. Ale jeśli inny wątek (w tym przypadku Thread-0) zdołał wejść do bow()metody, to blokada została już uzyskana i Thread-1czeka naThread-0, i wzajemnie. Ten impas jest nierozwiązywalny i nazywamy go impasem. Podobnie jak śmiertelny uścisk, którego nie można zwolnić, impas jest wzajemnym blokowaniem, którego nie można przełamać. Aby uzyskać inne wyjaśnienie impasu, możesz obejrzeć ten film: Objaśnienie impasu i Livelocka .

Blokada życia

Jeśli jest impas, czy jest też livelock? Tak, jest :) Livelock ma miejsce, gdy wątki na zewnątrz wydają się żywe, ale nie są w stanie nic zrobić, ponieważ warunek (warunki) wymagane do kontynuowania ich pracy nie mogą zostać spełnione. Zasadniczo blokada na żywo jest podobna do zakleszczenia, ale wątki nie „zawieszają się” w oczekiwaniu na monitor. Zamiast tego 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 uruchamiania wątków przez program planujący wątki Java. Jeśli Thead-1zaczyna się jako pierwszy, otrzymujemy 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 próbują po kolei uzyskać obie blokady, ale im się to nie udaje. Jednak nie są w impasie. Na zewnątrz wszystko jest w porządku i wykonują swoją pracę. Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 4Według JVisualVM widzimy okresy uśpienia i okres parkowania (wtedy wątek próbuje uzyskać blokadę — wchodzi w stan parkowania, o czym mówiliśmy wcześniej, gdy mówiliśmy o synchronizacji wątków ) . Możesz zobaczyć przykład livelock tutaj: Java - Thread Livelock .

Głód

Oprócz impasu i blokady na żywo istnieje inny problem, który może wystąpić podczas wielowątkowości: głód. Zjawisko to różni się od poprzednich form blokowania tym, że wątki nie są blokowane — po prostu nie mają wystarczających zasobów. W rezultacie, podczas gdy niektóre wątki zajmują cały czas wykonania, inne nie mogą działać: Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 5

https://www.logicbig.com/

Możesz zobaczyć super przykład tutaj: Java - Thread Starvation and Fairness . Ten przykład pokazuje, co dzieje się z nitkami podczas głodu i jak jedna mała zmiana z Thread.sleep()na Thread.wait()pozwala równomiernie rozłożyć obciążenie. Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 6

Warunki wyścigu

W wielowątkowości istnieje coś takiego jak „stan wyścigu”. Zjawisko to ma miejsce, gdy wątki współdzielą zasób, ale kod jest napisany w sposób, który nie zapewnia poprawnego udostępniania. Spójrz 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 generować błędu za pierwszym razem. Kiedy to nastąpi, 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ć, coś poszło nie tak podczas newValueprzypisywania wartości. newValuejest za duży. Ze względu na warunki wyścigu jeden z wątków zdołał zmienić zmienną valuemiędzy dwoma instrukcjami. Okazuje się, że między wątkami trwa wyścig. Pomyśl teraz, jak ważne jest, aby nie popełniać podobnych błędów przy transakcjach pieniężnych... Przykłady i diagramy można również zobaczyć tutaj: Kod do symulacji wyścigu w wątku Java .

Lotny

Mówiąc o interakcji wątków, volatilewarto wspomnieć o słowie kluczowym. 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;
    }
}
Co najciekawsze, jest bardzo prawdopodobne, że to nie zadziała. Nowy wątek nie zobaczy zmiany w flagpolu. Aby rozwiązać ten problem w polu flag, musimy użyć słowa volatilekluczowego. Jak i dlaczego? Procesor wykonuje wszystkie czynności. Ale wyniki obliczeń muszą być gdzieś przechowywane. Do tego jest pamięć główna i pamięć podręczna procesora. Pamięci podręczne procesora są jak mały fragment pamięci używany do szybszego dostępu do danych niż w przypadku dostępu do pamięci głównej. Ale wszystko ma swoje wady: dane w pamięci podręcznej mogą być nieaktualne (jak w powyższym przykładzie, gdy wartość pola flagi nie została zaktualizowana). Tak więcvolatilesłowo kluczowe mówi JVM, że nie chcemy buforować naszej zmiennej. Dzięki temu aktualny wynik będzie widoczny we wszystkich wątkach. Jest to bardzo uproszczone wyjaśnienie. Jeśli chodzi o volatilesłowo kluczowe, gorąco polecam przeczytanie tego artykułu . Aby uzyskać więcej informacji, radzę również przeczytać Java Memory Model i Java Volatile Keyword . Dodatkowo należy pamiętać, że volatilechodzi o widoczność, a nie atomowość zmian. Patrząc na kod w sekcji „Warunki wyścigu”, zobaczymy podpowiedź w IntelliJ IDEA: Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 7Ta 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 musi być niepodzielna. Niestety, operacja inkrementacji nie jest atomowa, ponieważ inkrementacja wymaga aż trzech operacji CPU: pobrać starą wartość, dodać do niej jedną, a następnie zapisać wartość. Dlaczego atomowość jest ważna? W przypadku operacji inkrementacji, jeśli występuje sytuacja wyścigu, współdzielony zasób (tj. wspólna wartość) może nagle zmienić się w dowolnym momencie. Ponadto operacje obejmujące struktury 64-bitowe, na przykład longi double, nie są niepodzielne. Więcej szczegółów można przeczytać tutaj: Zapewnij niepodzielność podczas odczytu i zapisu wartości 64-bitowych . Problemy związane z atomowością można zobaczyć w tym 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());
    }
}
Klasa specjalna AtomicIntegerzawsze da nam 30 000, ale valueco jakiś czas będzie się to zmieniać. Istnieje krótkie omówienie tego tematu: Wprowadzenie do zmiennych atomowych w Javie . Algorytm „porównaj i zamień” leży u podstaw klas atomowych. Więcej na ten temat można przeczytać tutaj w Porównanie algorytmów bezblokadowych - CAS i FAA na przykładzie JDK 7 i 8 lub w artykule Porównaj i zamień na Wikipedii. Razem lepiej: Java i klasa Thread.  Część III — Interakcja — 9

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

Dzieje się przed

Istnieje ciekawa i tajemnicza koncepcja zwana „dzieje się wcześniej”. W ramach studiowania wątków powinieneś o tym przeczytać. Relacja dzieje się przed pokazuje kolejność, w jakiej będą widoczne akcje między wątkami. Istnieje wiele interpretacji i komentarzy. Oto jedna z najnowszych prezentacji na ten temat: Java „Happens-Before” Relationships .

Streszczenie

W tej recenzji zbadaliśmy niektóre szczegóły interakcji wątków. Omówiliśmy problemy, które mogą się pojawić, a także sposoby ich identyfikacji i eliminacji. Lista dodatkowych materiałów na ten temat: Razem lepiej: Java i klasa Thread. Część I — Wątki wykonania Lepiej razem: Java i klasa Thread. Część II — Synchronizacja Lepiej razem: Java i klasa Thread. Część IV — Callable, Future i friends Razem lepiej: Java i klasa Thread. Część V — Executor, ThreadPool, Fork/Join Better together: Java i klasa Thread. Część VI — Odpalaj!
Komentarze
  • Popularne
  • Najnowsze
  • Najstarsze
Musisz się zalogować, aby dodać komentarz
Ta strona nie ma jeszcze żadnych komentarzy