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 . Wątki są niezbędne do wykonywania pracy równolegle. To sprawia, że jest wysoce prawdopodobne, że wątki będą w jakiś sposób oddziaływać na siebie. Przyjrzyjmy się, jak to się dzieje i jakimi podstawowymi narzędziami dysponujemy.Dawać
Thread.yield() jest kłopotliwy i rzadko używany. W Internecie jest to opisywane na wiele różnych sposobów. W tym niektórzy ludzie piszą, że jest jakaś kolejka wątków, w której wątek będzie opadał na podstawie priorytetów wątków. Inni piszą, że wątek zmieni swój status z „Running” na „Runnable” (chociaż nie ma rozróżnienia między tymi statusami, tj. Java ich nie rozróżnia). Rzeczywistość jest taka, że to wszystko jest znacznie mniej znane, a jednak w pewnym sensie prostsze. Wystąpił błąd ( JDK-6416721: (spec thread) Fix Thread.yield() javadoc ) zarejestrowany wyield()
dokumentacji metody. Jeśli to przeczytasz, stanie się jasne, żeyield()
w rzeczywistości zawiera tylko pewne zalecenia dla programu planującego wątki Java, że ten wątek może mieć krótszy czas wykonania. Ale to, co faktycznie się dzieje, tj. czy program planujący działa zgodnie z zaleceniami i co ogólnie robi, zależy od implementacji JVM i systemu operacyjnego. A może to też zależeć od innych czynników. Całe zamieszanie wynika najprawdopodobniej z faktu, że wielowątkowość została ponownie przemyślana wraz z rozwojem języka Java. Przeczytaj więcej w przeglądzie tutaj: Krótkie wprowadzenie do Java Thread.yield() .
Spać
Wątek może przejść w stan uśpienia podczas wykonywania. Jest to najłatwiejszy rodzaj interakcji z innymi wątkami. System operacyjny, na którym działa wirtualna maszyna Java, na której działa nasz kod Java, ma własny harmonogram wątków . Decyduje, który wątek rozpocząć i kiedy. Programista nie może wchodzić w interakcje z tym harmonogramem bezpośrednio z kodu Java, tylko za pośrednictwem maszyny JVM. Może poprosić program planujący o wstrzymanie wątku na chwilę, czyli uśpienie go. Możesz przeczytać więcej w tych artykułach: Thread.sleep() i Jak działa wielowątkowość . Możesz także sprawdzić, jak działają wątki w systemach operacyjnych Windows: Internals of Windows Thread . A teraz zobaczmy to na własne oczy. Zapisz następujący kod w pliku o nazwieHelloWorldApp.java
:
class HelloWorldApp {
public static void main(String []args) {
Runnable task = () -> {
try {
int secToWait = 1000 * 60;
Thread.currentThread().sleep(secToWait);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(task);
thread.start();
}
}
Jak widać mamy jakieś zadanie, które czeka 60 sekund, po czym program się kończy. Kompilujemy za pomocą polecenia „ javac HelloWorldApp.java
”, a następnie uruchamiamy program za pomocą „ java HelloWorldApp
”. Program najlepiej uruchomić w osobnym oknie. Na przykład w systemie Windows wygląda to tak: start java HelloWorldApp
. Używamy polecenia jps, aby uzyskać PID (identyfikator procesu) i otwieramy listę wątków za pomocą „ jvisualvm --openpid pid
: Jak widać, nasz wątek ma teraz status „Uśpiony”. W rzeczywistości istnieje bardziej elegancki sposób pomocy nasz wątek ma słodkie sny:
try {
TimeUnit.SECONDS.sleep(60);
System.out.println("Woke up");
} catch (InterruptedException e) {
e.printStackTrace();
}
Czy zauważyliście, że zajmujemy się InterruptedException
wszędzie? Zrozummy dlaczego.
przerwanie wątku()
Chodzi o to, że gdy wątek czeka/uśpi, ktoś może chcieć przerwać. W tym przypadku zajmujemy sięInterruptedException
. Mechanizm ten powstał po Thread.stop()
zadeklarowaniu metody jako Deprecated, czyli przestarzałej i niepożądanej. Powodem było to, że po stop()
wywołaniu metody wątek został po prostu „zabity”, co było bardzo nieprzewidywalne. Nie mogliśmy wiedzieć, kiedy wątek zostanie zatrzymany, i nie mogliśmy zagwarantować spójności danych. Wyobraź sobie, że zapisujesz dane do pliku, podczas gdy wątek jest zabijany. Zamiast zabijać wątek, twórcy Javy zdecydowali, że bardziej logicznie będzie powiedzieć mu, że powinien zostać przerwany. To, jak odpowiedzieć na te informacje, zależy od samego wątku. Aby uzyskać więcej informacji, przeczytaj Dlaczego Thread.stop jest przestarzały?na stronie internetowej Oracle. Spójrzmy na przykład:
public static void main(String []args) {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
W tym przykładzie nie będziemy czekać 60 sekund. Zamiast tego natychmiast wyświetlimy komunikat „Przerwano”. Dzieje się tak, ponieważ wywołaliśmy interrupt()
metodę w wątku. Ta metoda ustawia wewnętrzną flagę o nazwie „stan przerwania”. Oznacza to, że każdy wątek ma wewnętrzną flagę, która nie jest bezpośrednio dostępna. Ale mamy natywne metody interakcji z tą flagą. Ale to nie jedyny sposób. Wątek może działać, nie czekać na coś, po prostu wykonywać akcje. Ale może przewidywać, że inni będą chcieli zakończyć jego pracę w określonym czasie. Na przykład:
public static void main(String []args) {
Runnable task = () -> {
while(!Thread.currentThread().isInterrupted()) {
// Do some work
}
System.out.println("Finished");
};
Thread thread = new Thread(task);
thread.start();
thread.interrupt();
}
W powyższym przykładzie while
pętla będzie wykonywana, dopóki wątek nie zostanie przerwany z zewnątrz. Jeśli chodzi o isInterrupted
flagę, ważne jest, aby wiedzieć, że jeśli złapiemy InterruptedException
, flaga isInterrupted zostanie zresetowana, a następnie isInterrupted()
zwróci fałsz. Klasa Thread ma również statyczną metodę Thread.interrupted() , która ma zastosowanie tylko do bieżącego wątku, ale ta metoda resetuje flagę do wartości false! Przeczytaj więcej w tym rozdziale zatytułowanym Przerwanie wątku .
Dołącz (poczekaj na zakończenie innego wątku)
Najprostszym rodzajem oczekiwania jest oczekiwanie na zakończenie innego wątku.
public static void main(String []args) throws InterruptedException {
Runnable task = () -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
};
Thread thread = new Thread(task);
thread.start();
thread.join();
System.out.println("Finished");
}
W tym przykładzie nowy wątek będzie uśpiony przez 5 sekund. W tym samym czasie główny wątek będzie czekał, aż uśpiony wątek się obudzi i zakończy swoją pracę. Jeśli spojrzysz na stan wątku w JVisualVM, to będzie on wyglądał tak: Dzięki narzędziom monitorującym możesz zobaczyć, co dzieje się z wątkiem. Metoda join
jest dość prosta, ponieważ jest to po prostu metoda z kodem Java, która jest wykonywana wait()
tak długo, jak długo żyje wątek, w którym została wywołana. Gdy tylko wątek umrze (kiedy zakończy swoją pracę), oczekiwanie zostaje przerwane. I to jest cała magia metody join()
. Przejdźmy więc do najciekawszej rzeczy.
Monitor
Wielowątkowość obejmuje koncepcję monitora. Słowo monitor pochodzi z języka angielskiego z XVI-wiecznej łaciny i oznacza „instrument lub urządzenie używane do obserwacji, sprawdzania lub ciągłego rejestrowania procesu”. W kontekście tego artykułu postaramy się omówić podstawy. Wszystkich, którzy chcą poznać szczegóły, odsyłamy do powiązanych materiałów. Naszą podróż zaczynamy od specyfikacji języka Java (JLS): 17.1. Synchronizacja . Mówi, co następuje: Okazuje się, że Java używa mechanizmu „monitorowania” do synchronizacji między wątkami. Monitor jest powiązany z każdym obiektem, a wątki mogą go uzyskaćlock()
lub zwolnić za pomocą unlock()
. Następnie na stronie Oracle znajdziemy samouczek: Intrinsic Locks and Synchronization. Ten samouczek mówi, że synchronizacja w Javie jest zbudowana wokół wewnętrznej jednostki zwanej wewnętrzną blokadą lub blokadą monitora . Ta blokada jest często nazywana po prostu „ monitorem ”. Widzimy również ponownie, że każdy obiekt w Javie ma powiązaną wewnętrzną blokadę. Możesz przeczytać Java — wewnętrzne blokady i synchronizacja . Następnie ważne będzie zrozumienie, w jaki sposób obiekt w Javie może być powiązany z monitorem. W Javie każdy obiekt ma nagłówek, w którym przechowywane są wewnętrzne metadane niedostępne dla programisty z kodu, ale których maszyna wirtualna potrzebuje do poprawnej pracy z obiektami. Nagłówek obiektu zawiera „słowo znaku”, które wygląda następująco:
https://edu.netbeans.org/contrib/slides/java-overview-and-java-se6.pdf
public class HelloWorld{
public static void main(String []args){
Object object = new Object();
synchronized(object) {
System.out.println("Hello World");
}
}
}
Tutaj bieżący wątek (ten, w którym wykonywane są te wiersze kodu) używa słowa synchronized
kluczowego, aby spróbować użyć monitora powiązanego zobject"\
zmienna, aby uzyskać/uzyskać blokadę. Jeśli nikt inny nie walczy o monitor (tj. nikt inny nie uruchamia zsynchronizowanego kodu przy użyciu tego samego obiektu), Java może próbować przeprowadzić optymalizację zwaną „blokowaniem stronniczym”. Do słowa znacznika w nagłówku obiektu dodawany jest odpowiedni znacznik oraz zapis o tym, który wątek posiada blokadę monitora. Zmniejsza to narzut wymagany do zablokowania monitora. Jeśli monitor był wcześniej własnością innego wątku, to takie blokowanie nie wystarczy. JVM przełącza się na następny typ blokowania: „blokowanie podstawowe”. Wykorzystuje operacje porównania i zamiany (CAS). Co więcej, samo słowo znaku nagłówka obiektu nie przechowuje już słowa znaku, ale raczej odniesienie do miejsca, w którym jest przechowywane, a znacznik zmienia się, aby JVM zrozumiała, że używamy podstawowego blokowania. Jeśli wiele wątków konkuruje (rywalizuje) o monitor (jeden uzyskał blokadę, a drugi czeka na zwolnienie blokady), wówczas znacznik w słowie znaku zmienia się, a słowo znaku przechowuje teraz odniesienie do monitora jako obiekt — jakiś wewnętrzny byt JVM. Jak stwierdzono w JDK Enchancement Proposal (JEP), ta sytuacja wymaga miejsca w obszarze Native Heap w celu przechowywania tej jednostki. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. a drugi czeka na zwolnienie blokady), a następnie zmienia się znacznik w słowie znakowym, a słowo znacznikowe przechowuje teraz odniesienie do monitora jako obiekt — jakąś wewnętrzną jednostkę JVM. Jak stwierdzono w JDK Enchancement Proposal (JEP), ta sytuacja wymaga miejsca w obszarze Native Heap w celu przechowywania tej jednostki. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. a drugi czeka na zwolnienie blokady), a następnie zmienia się znacznik w słowie znakowym, a słowo znacznikowe przechowuje teraz odniesienie do monitora jako obiekt — jakąś wewnętrzną jednostkę JVM. Jak stwierdzono w JDK Enchancement Proposal (JEP), ta sytuacja wymaga miejsca w obszarze Native Heap w celu przechowywania tej jednostki. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. a słowo znakowe przechowuje teraz odniesienie do monitora jako obiektu — jakiejś wewnętrznej jednostki JVM. Jak stwierdzono w JDK Enchancement Proposal (JEP), ta sytuacja wymaga miejsca w obszarze Native Heap w celu przechowywania tej jednostki. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. a słowo znakowe przechowuje teraz odniesienie do monitora jako obiektu — jakiejś wewnętrznej jednostki JVM. Jak stwierdzono w JDK Enchancement Proposal (JEP), ta sytuacja wymaga miejsca w obszarze Native Heap w celu przechowywania tej jednostki. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach. Odniesienie do lokalizacji pamięci tej jednostki wewnętrznej będzie przechowywane w słowie znakowym nagłówka obiektu. Tak więc monitor jest tak naprawdę mechanizmem synchronizacji dostępu do współdzielonych zasobów między wieloma wątkami. JVM przełącza się między kilkoma implementacjami tego mechanizmu. Tak więc, dla uproszczenia, mówiąc o monitorze, tak naprawdę mówimy o zamkach.
Zsynchronizowane (oczekiwanie na blokadę)
Jak widzieliśmy wcześniej, koncepcja „bloku zsynchronizowanego” (lub „sekcji krytycznej”) jest ściśle związana z koncepcją monitora. Spójrz na przykład:
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Runnable task = () -> {
synchronized(lock) {
System.out.println("thread");
}
};
Thread th1 = new Thread(task);
th1.start();
synchronized(lock) {
for (int i = 0; i < 8; i++) {
Thread.currentThread().sleep(1000);
System.out.print(" " + i);
}
System.out.println(" ...");
}
}
Tutaj główny wątek najpierw przekazuje obiekt zadania do nowego wątku, a następnie natychmiast uzyskuje blokadę i wykonuje na nim długą operację (8 sekund). Przez cały ten czas zadanie nie może być kontynuowane, ponieważ nie może wejść do synchronized
bloku, ponieważ zamek jest już zdobyty. Jeśli wątek nie może uzyskać blokady, będzie czekał na monitor. Gdy tylko dostanie blokadę, będzie kontynuować wykonywanie. Gdy wątek opuszcza monitor, zwalnia blokadę. W JVisualVM wygląda to tak: Jak widać w JVisualVM, status to „Monitor”, co oznacza, że wątek jest zablokowany i nie może przejąć monitora. Możesz także użyć kodu do określenia statusu wątku, ale nazwy statusów określone w ten sposób nie odpowiadają nazwom używanym w JVisualVM, chociaż są podobne. W tym przypadkuth1.getState()
Instrukcja w pętli for zwróci BLOCKED , ponieważ dopóki pętla jest uruchomiona, lock
monitor obiektu jest zajęty przez main
wątek, a th1
wątek jest zablokowany i nie może kontynuować, dopóki blokada nie zostanie zwolniona. Oprócz zsynchronizowanych bloków można zsynchronizować całą metodę. Na przykład oto metoda z HashTable
klasy:
public synchronized int size() {
return count;
}
Ta metoda będzie wykonywana tylko przez jeden wątek w danym momencie. Czy naprawdę potrzebujemy zamka? Tak, potrzebujemy tego. W przypadku metod instancji obiekt „ten” (obiekt bieżący) pełni rolę blokady. Interesująca dyskusja na ten temat toczy się tutaj: Czy stosowanie metody zsynchronizowanej zamiast zsynchronizowanego bloku ma zalety? . Jeśli metoda jest statyczna, wówczas blokadą nie będzie obiekt „ten” (ponieważ nie ma obiektu „ten” dla metody statycznej), ale raczej obiekt klasy (na przykład ) Integer.class
.
Czekaj (czekając na monitor). metody powiadamiania() i powiadamianiaWszystkie().
Klasa Thread ma inną metodę oczekiwania, która jest powiązana z monitorem. W przeciwieństwie dosleep()
and join()
, tej metody nie można po prostu wywołać. Jego nazwa to wait()
. Metoda wait
jest wywoływana na obiekcie powiązanym z monitorem, na który chcemy czekać. Zobaczmy przykład:
public static void main(String []args) throws InterruptedException {
Object lock = new Object();
// The task object will wait until it is notified via lock
Runnable task = () -> {
synchronized(lock) {
try {
lock.wait();
} catch(InterruptedException e) {
System.out.println("interrupted");
}
}
// After we are notified, we will wait until we can acquire the lock
System.out.println("thread");
};
Thread taskThread = new Thread(task);
taskThread.start();
// We sleep. Then we acquire the lock, notify, and release the lock
Thread.currentThread().sleep(3000);
System.out.println("main");
synchronized(lock) {
lock.notify();
}
}
W JVisualVM wygląda to tak: Aby zrozumieć, jak to działa, pamiętaj, że metody wait()
i notify()
są powiązane z java.lang.Object
. Może wydawać się dziwne, że w Object
klasie znajdują się metody związane z wątkami. Ale przyczyna tego teraz się ujawnia. Jak zapewne pamiętasz, każdy obiekt w Javie ma nagłówek. Nagłówek zawiera różne informacje porządkowe, w tym informacje o monitorze, tj. o stanie zamka. Pamiętaj, że każdy obiekt lub instancja klasy jest powiązana z wewnętrzną jednostką w JVM, zwaną wewnętrzną blokadą lub monitorem. W powyższym przykładzie kod obiektu zadania wskazuje, że wprowadzamy blok synchronizacji dla monitora powiązanego z obiektem lock
. Jeśli uda nam się zdobyć blokadę dla tego monitora, towait()
jest nazywany. Wątek wykonujący zadanie zwolni lock
monitor obiektu, ale wejdzie do kolejki wątków oczekujących na powiadomienie z lock
monitora obiektu. Ta kolejka wątków nazywa się ZESTAWEM OCZEKIWANIA, co lepiej odzwierciedla jej cel. Oznacza to, że jest to bardziej zestaw niż kolejka. Wątek main
tworzy nowy wątek z obiektem zadania, uruchamia go i czeka 3 sekundy. To sprawia, że jest bardzo prawdopodobne, że nowy wątek będzie w stanie uzyskać blokadę przed wątkiem main
i dostać się do kolejki monitora. Następnie main
sam wątek wchodzi do lock
zsynchronizowanego bloku obiektu i wykonuje powiadomienie wątku za pomocą monitora. Po wysłaniu powiadomienia wątek main
zwalnialock
monitor obiektu, a nowy wątek, który wcześniej czekał na lock
zwolnienie monitora obiektu, kontynuuje wykonywanie. Możliwe jest wysłanie powiadomienia tylko do jednego wątku ( notify()
) lub jednocześnie do wszystkich wątków w kolejce ( notifyAll()
). Przeczytaj więcej tutaj: Różnica między powiadomieniem() a powiadomieniemAll() w Javie . Należy zauważyć, że kolejność powiadomień zależy od sposobu implementacji JVM. Przeczytaj więcej tutaj: Jak rozwiązać problem głodu za pomocą powiadomień i powiadomień? . Synchronizację można przeprowadzić bez określania obiektu. Można to zrobić, gdy synchronizowana jest cała metoda, a nie pojedynczy blok kodu. Na przykład dla metod statycznych blokadą będzie obiekt klasy (uzyskany za pomocą .class
):
public static synchronized void printA() {
System.out.println("A");
}
public static void printB() {
synchronized(HelloWorld.class) {
System.out.println("B");
}
}
Jeśli chodzi o używanie zamków, obie metody są takie same. Jeśli metoda nie jest statyczna, to synchronizacja zostanie przeprowadzona przy użyciu bieżącego instance
, czyli przy użyciu this
. Nawiasem mówiąc, powiedzieliśmy wcześniej, że możesz użyć getState()
metody, aby uzyskać status wątku. Na przykład dla wątku w kolejce oczekującego na monitor statusem będzie WAITING lub TIMED_WAITING, jeśli metoda wait()
określiła limit czasu.
https://stackoverflow.com/questions/36425942/what-is-the-cycle-of-thread-in-java
Cykl życia nici
W trakcie swojego życia status wątku zmienia się. W rzeczywistości zmiany te składają się na cykl życia nici. Gdy tylko wątek zostanie utworzony, jego status to NOWY. W tym stanie nowy wątek nie jest jeszcze uruchomiony, a harmonogram wątków Java jeszcze nic o nim nie wie. Aby program planujący wątki dowiedział się o wątku, musisz wywołaćthread.start()
metodę. Następnie wątek przejdzie do stanu RUNNABLE. Internet zawiera wiele niepoprawnych diagramów, które rozróżniają stany „Runnable” i „Running”. Ale to błąd, ponieważ Java nie rozróżnia „gotowy do pracy” (uruchamiany) i „działający” (uruchamiany). Kiedy wątek jest żywy, ale nieaktywny (nie jest Runnable), znajduje się w jednym z dwóch stanów:
- ZABLOKOWANY — oczekiwanie na wejście w sekcję krytyczną, czyli blokadę
synchronized
. - WAITING — oczekiwanie, aż inny wątek spełni jakiś warunek.
getState()
metody. Wątki mają również isAlive()
metodę, która zwraca wartość true, jeśli wątek nie jest ZAKOŃCZONY.
BlokadaWsparcie i parkowanie nici
Począwszy od Javy 1.6 pojawił się ciekawy mechanizm o nazwie LockSupport . Ta klasa kojarzy „zezwolenie” z każdym wątkiem, który go używa. Wywołanie metodypark()
zwraca natychmiast, jeśli zezwolenie jest dostępne, zużywając zezwolenie w procesie. W przeciwnym razie blokuje. Wywołanie unpark
metody udostępnia zezwolenie, jeśli jeszcze nie jest dostępne. Jest tylko 1 zezwolenie. Dokumentacja Java dla LockSupport
odnosi się do Semaphore
klasy. Spójrzmy na prosty przykład:
import java.util.concurrent.Semaphore;
public class HelloWorldApp{
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0);
try {
semaphore.acquire();
} catch (InterruptedException e) {
// Request the permit and wait until we get it
e.printStackTrace();
}
System.out.println("Hello, World!");
}
}
Ten kod będzie zawsze czekał, ponieważ teraz semafor ma 0 zezwoleń. A kiedy acquire()
w kodzie zostanie wywołana (tj. request the allow), wątek czeka, aż otrzyma zezwolenie. Ponieważ czekamy, musimy sobie poradzić InterruptedException
. Co ciekawe, semafor otrzymuje osobny stan wątku. Jeśli spojrzymy w JVisualVM, zobaczymy, że stan to nie „Czekaj”, ale „Park”. Spójrzmy na inny przykład:
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Park the current thread
System.err.println("Will be Parked");
LockSupport.park();
// As soon as we are unparked, we will start to act
System.err.println("Unparked");
};
Thread th = new Thread(task);
th.start();
Thread.currentThread().sleep(2000);
System.err.println("Thread state: " + th.getState());
LockSupport.unpark(th);
Thread.currentThread().sleep(2000);
}
Status wątku będzie OCZEKIWAŁ, ale JVisualVM rozróżnia wait
słowo synchronized
kluczowe i park
klasę LockSupport
. Dlaczego jest to LockSupport
takie ważne? Wracamy ponownie do dokumentacji Java i patrzymy na stan wątku WAITING . Jak widać, są tylko trzy sposoby, aby się do niego dostać. Dwa z tych sposobów to wait()
i join()
. A trzeci jest LockSupport
. W Javie zamki można również budować na LockSuppor
ti i oferować narzędzia wyższego poziomu. Spróbujmy użyć jednego. Na przykład spójrz na ReentrantLock
:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class HelloWorld{
public static void main(String []args) throws InterruptedException {
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
System.out.println("Thread");
lock.unlock();
};
lock.lock();
Thread th = new Thread(task);
th.start();
System.out.println("main");
Thread.currentThread().sleep(2000);
lock.unlock();
}
}
Podobnie jak w poprzednich przykładach, tutaj wszystko jest proste. Obiekt lock
czeka, aż ktoś zwolni udostępniony zasób. Jeśli spojrzymy w JVisualVM, zobaczymy, że nowy wątek będzie zaparkowany, dopóki wątek main
nie zwolni blokady. Możesz przeczytać więcej o blokadach tutaj: Java 8 StampedLocks vs. ReadWriteLocks oraz Synchronized and Lock API w Javie. Aby lepiej zrozumieć sposób implementacji blokad, warto przeczytać o Phaserze w tym artykule: Przewodnik po Java Phaser . Mówiąc o różnych synchronizatorach, musisz przeczytać artykuł DZone na temat synchronizatorów Java.
Wniosek
W tym przeglądzie przyjrzeliśmy się głównym sposobom interakcji wątków w Javie. Dodatkowy materiał:- Razem lepiej: Java i klasa Thread. Część I — Wątki egzekucyjne
- https://zone.com/articles/the-java-synchronizers
- https://www.javatpoint.com/java-multithreading-interview-questions
GO TO FULL VERSION