Wstęp
Wątki to ciekawa rzecz. W poprzednich recenzjach przyjrzeliśmy się niektórym dostępnym narzędziom do implementacji wielowątkowości. Zobaczmy, jakie inne ciekawe rzeczy możemy zrobić. W tym momencie wiemy bardzo dużo. Na przykład z „
Lepiej razem: Java i klasa Thread. Część I — Wątki wykonania ” wiemy, że klasa Thread reprezentuje wątek wykonania. Wiemy, że wątek wykonuje jakieś zadanie. Jeśli chcemy, aby nasze zadania mogły to robić
run
, musimy oznaczyć wątek za pomocą
Runnable
.
![Razem lepiej: Java i klasa Thread. Część VI — Odpal! - 1]()
Dla przypomnienia możemy skorzystać z
kompilatora Java Tutorialspoint Online :
public static void main(String[] args){
Runnable task = () -> {
Thread thread = Thread.currentThread();
System.out.println("Hello from " + thread.getName());
};
Thread thread = new Thread(task);
thread.start();
}
Wiemy też, że mamy coś, co nazywamy zamkiem. Dowiedzieliśmy się o tym w „
Lepiej razem: Java i klasa Thread. Część II — Synchronizacja . Jeśli jeden wątek uzyska blokadę, to inny wątek próbujący uzyskać blokadę będzie zmuszony czekać na zwolnienie blokady:
import java.util.concurrent.locks.*;
public class HelloWorld{
public static void main(String []args){
Lock lock = new ReentrantLock();
Runnable task = () -> {
lock.lock();
Thread thread = Thread.currentThread();
System.out.println("Hello from " + thread.getName());
lock.unlock();
};
Thread thread = new Thread(task);
thread.start();
}
}
Myślę, że nadszedł czas, aby porozmawiać o tym, jakie inne ciekawe rzeczy możemy robić.
Semafory
Najprostszym sposobem kontrolowania, ile wątków może działać jednocześnie, jest semafor. To jak sygnał kolejowy. Zielony oznacza kontynuację. Czerwony oznacza czekać. Czekaj na co z semafora? Dostęp. Aby uzyskać dostęp, musimy go zdobyć. A kiedy dostęp nie jest już potrzebny, musimy go oddać lub zwolnić. Zobaczmy, jak to działa. Musimy zaimportować
java.util.concurrent.Semaphore
klasę. Przykład:
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(0);
Runnable task = () -> {
try {
semaphore.acquire();
System.out.println("Finished");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
Thread.sleep(5000);
semaphore.release(1);
}
Jak widać, te operacje (acquire i release) pomagają nam zrozumieć, jak działa semafor. Najważniejsze jest to, że jeśli mamy uzyskać dostęp, to semafor musi mieć dodatnią liczbę zezwoleń. Ta liczba może być zainicjowana liczbą ujemną. I możemy poprosić (zdobyć) więcej niż 1 pozwolenie.
Odliczanie Zatrzask
Kolejnym mechanizmem jest
CountDownLatch
. Nic dziwnego, że jest to zatrzask z odliczaniem. Tutaj potrzebujemy odpowiedniej instrukcji importu dla
java.util.concurrent.CountDownLatch
klasy. To jak wyścig pieszy, w którym wszyscy zbierają się na linii startu. A kiedy wszyscy są gotowi, wszyscy otrzymują sygnał startu w tym samym czasie i jednocześnie startują. Przykład:
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);
Runnable task = () -> {
try {
countDownLatch.countDown();
System.out.println("Countdown: " + countDownLatch.getCount());
countDownLatch.await();
System.out.println("Finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
Najpierw mówimy zatrzaskowi, aby
countDown()
. Google definiuje odliczanie jako „akt liczenia cyfr w odwrotnej kolejności do zera”. Następnie mówimy zatrzaskowi, aby
await()
, czyli czekał, aż licznik osiągnie zero. Co ciekawe, jest to licznik jednorazowy. Dokumentacja Java mówi: „Gdy wątki muszą wielokrotnie odliczać w ten sposób, zamiast tego użyj CyclicBarrier”. Innymi słowy, jeśli potrzebujesz licznika wielokrotnego użytku, potrzebujesz innej opcji:
CyclicBarrier
.
Cykliczna Bariera
Jak sama nazwa wskazuje,
CyclicBarrier
jest to bariera „wielokrotnego użytku”. Będziemy musieli zaimportować
java.util.concurrent.CyclicBarrier
klasę. Spójrzmy na przykład:
public static void main(String[] args) throws InterruptedException {
Runnable action = () -> System.out.println("On your mark!");
CyclicBarrier barrier = new CyclicBarrier(3, action);
Runnable task = () -> {
try {
barrier.await();
System.out.println("Finished");
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
};
System.out.println("Limit: " + barrier.getParties());
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
}
Jak widać, wątek uruchamia
await
metodę, czyli czeka. W tym przypadku wartość bariery maleje. Bariera jest uważana za przełamaną (
barrier.isBroken()
), gdy odliczanie osiągnie zero. Aby zresetować barierę, musisz wywołać
reset()
metodę, której
CountDownLatch
nie ma.
Wymiennik
Kolejnym mechanizmem jest Exchanger. W tym kontekście Giełda jest punktem synchronizacji, w którym rzeczy są wymieniane lub wymieniane. Jak można się spodziewać, an
Exchanger
jest klasą, która wykonuje wymianę lub zamianę. Spójrzmy na najprostszy przykład:
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Runnable task = () -> {
try {
Thread thread = Thread.currentThread();
String withThreadName = exchanger.exchange(thread.getName());
System.out.println(thread.getName() + " exchanged with " + withThreadName);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
new Thread(task).start();
}
Tutaj zaczynamy dwa wątki. Każdy z nich uruchamia metodę wymiany i czeka, aż drugi wątek również uruchomi metodę wymiany. W ten sposób wątki wymieniają przekazane argumenty. Ciekawy. Nie przypomina ci to czegoś? Przypomina
SynchronousQueue
, który leży u podstaw
CachedThreadPool
. Dla jasności oto przykład:
public static void main(String[] args) throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue<>();
Runnable task = () -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(task).start();
queue.put("Message");
}
Przykład pokazuje, że po uruchomieniu nowego wątku będzie on czekał, ponieważ kolejka będzie pusta. A następnie główny wątek umieszcza ciąg „Wiadomość” w kolejce. Co więcej, zatrzyma się również, dopóki ten ciąg nie zostanie odebrany z kolejki. Możesz także przeczytać „
SynchronousQueue vs Exchanger ”, aby dowiedzieć się więcej na ten temat.
Fazer
Najlepsze zostawiliśmy na koniec —
Phaser
. Będziemy musieli zaimportować
java.util.concurrent.Phaser
klasę. Spójrzmy na prosty przykład:
public static void main(String[] args) throws InterruptedException {
Phaser phaser = new Phaser();
// By calling the register method, we register the current (main) thread as a party
phaser.register();
System.out.println("Phasecount is " + phaser.getPhase());
testPhaser(phaser);
testPhaser(phaser);
testPhaser(phaser);
// After 3 seconds, we arrive at the barrier and deregister. Number of arrivals = number of registrations = start
Thread.sleep(3000);
phaser.arriveAndDeregister();
System.out.println("Phasecount is " + phaser.getPhase());
}
private static void testPhaser(final Phaser phaser) {
// We indicate that there will be a +1 party on the Phaser
phaser.register();
// Start a new thread
new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + " arrived");
phaser.arriveAndAwaitAdvance(); // The threads register arrival at the phaser.
System.out.println(name + " after passing barrier");
}).start();
}
Przykład ilustruje, że przy użyciu
Phaser
, bariera pęka, gdy liczba rejestracji odpowiada liczbie przybyć do bariery. Możesz zapoznać się z tym
Phaser
, czytając
ten artykuł GeeksforGeeks .
Streszczenie
Jak widać z tych przykładów, istnieją różne sposoby synchronizacji wątków. Wcześniej próbowałem przypomnieć sobie aspekty wielowątkowości. Mam nadzieję, że poprzednie części z tej serii były przydatne. Niektórzy twierdzą, że droga do wielowątkowości zaczyna się od książki „Java Concurrency in Practice”. Chociaż została wydana w 2006 roku, ludzie mówią, że jest dość fundamentalna i nadal aktualna. Na przykład możesz przeczytać dyskusję tutaj:
Czy „Java Concurrency In Practice” jest nadal aktualna? . Przydatne jest również czytanie linków w dyskusji. Na przykład istnieje łącze do książki
The Well-Grounded Java Developer , aw szczególności wspomnimy o
rozdziale 4. Nowoczesna współbieżność . Jest też cała recenzja na ten temat:
Czy „współbieżność Java w praktyce” jest nadal aktualna w erze Java 8? Ten artykuł zawiera również sugestie dotyczące tego, co jeszcze warto przeczytać, aby naprawdę zrozumieć ten temat. Następnie możesz zajrzeć do świetnej książki, takiej jak
OCA/OCP Java SE 8 Programmer Practice Tests . Nas interesuje drugi akronim: OCP (Oracle Certified Professional). Testy znajdziesz w „Rozdziale 20: Współbieżność Java”. Ta książka zawiera zarówno pytania, jak i odpowiedzi z wyjaśnieniami. Na przykład:
![Razem lepiej: Java i klasa Thread. Część VI — Odpal! - 3]()
Wiele osób może zacząć mówić, że to pytanie jest kolejnym przykładem zapamiętywania metod. Z jednej strony tak. Z drugiej strony możesz odpowiedzieć na to pytanie, przypominając sobie, że
ExecutorService
jest to swego rodzaju „uaktualnienie”
Executor
. I
Executor
ma na celu po prostu ukryć sposób tworzenia wątków, ale nie jest to główny sposób ich wykonywania, czyli uruchamiania obiektu
Runnable
w nowym wątku. Dlatego nie ma
execute(Callable)
— ponieważ w
ExecutorService
,
Executor
po prostu dodaje
submit()
metody, które mogą zwrócić
Future
obiekt. Oczywiście możemy zapamiętać listę metod, ale o wiele łatwiej jest udzielić odpowiedzi w oparciu o naszą wiedzę o naturze samych klas. A oto kilka 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ęść III — Interakcja Lepiej razem: Java i klasa Thread. Część IV — Callable, Future i friends Razem lepiej: Java i klasa Thread. Część V — Executor, ThreadPool, Fork/Join
GO TO FULL VERSION