Einführung
Threads sind eine interessante Sache. In früheren Rezensionen haben wir uns einige der verfügbaren Tools zur Implementierung von Multithreading angesehen. Mal sehen, welche anderen interessanten Dinge wir tun können. Zu diesem Zeitpunkt wissen wir viel.
Aus „ Besser zusammen: Java und die Thread-Klasse. Teil I – Ausführungsthreads “ wissen wir beispielsweise , dass die Thread-Klasse einen Ausführungsthread darstellt. Wir wissen, dass ein Thread eine Aufgabe ausführt. Wenn wir möchten, dass unsere Aufgaben dies können
run
, müssen wir den Thread mit markieren
Runnable
.
Zur Erinnerung können wir den Tutorialspoint Online Java Compiler verwenden :
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();
}
Wir wissen auch, dass wir ein sogenanntes Schloss haben. Wir haben darüber in „
Besser zusammen: Java und die Thread-Klasse. Teil II – Synchronisierung“ erfahren. Wenn ein Thread eine Sperre erwirbt, muss ein anderer Thread, der versucht, die Sperre zu erlangen, auf die Freigabe der Sperre warten:
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();
}
}
Ich denke, es ist an der Zeit, darüber zu sprechen, welche anderen interessanten Dinge wir tun können.
Semaphore
Der einfachste Weg, um zu steuern, wie viele Threads gleichzeitig ausgeführt werden können, ist ein Semaphor. Es ist wie ein Eisenbahnsignal. Grün bedeutet fortfahren. Rot bedeutet warten. Was erwartet Sie vom Semaphor? Zugang. Um Zugang zu erhalten, müssen wir es erwerben. Und wenn der Zugriff nicht mehr benötigt wird, müssen wir ihn verschenken oder freigeben. Mal sehen, wie das funktioniert. Wir müssen die
java.util.concurrent.Semaphore
Klasse importieren. Beispiel:
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);
}
Wie Sie sehen, helfen uns diese Vorgänge (Erfassen und Freigeben) zu verstehen, wie ein Semaphor funktioniert. Das Wichtigste ist, dass der Semaphor eine positive Anzahl an Genehmigungen haben muss, um Zugang zu erhalten. Dieser Zähler kann auf eine negative Zahl initialisiert werden. Und wir können mehr als eine Genehmigung beantragen (erwerben).
CountDownLatch
Der nächste Mechanismus ist
CountDownLatch
. Es überrascht nicht, dass es sich hierbei um einen Riegel mit Countdown handelt. Hier benötigen wir die entsprechende Importanweisung für die
java.util.concurrent.CountDownLatch
Klasse. Es ist wie ein Wettlauf, bei dem sich alle an der Startlinie versammeln. Und wenn alle bereit sind, erhalten alle gleichzeitig den Startschuss und starten gleichzeitig. Beispiel:
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();
}
}
Zuerst weisen wir den Riegel an
countDown()
. Google definiert Countdown als „einen Vorgang, bei dem Zahlen in umgekehrter Reihenfolge bis Null gezählt werden“. Und dann sagen wir dem Latch
await()
, dass er warten soll, bis der Zähler Null wird. Interessanterweise handelt es sich hierbei um einen einmaligen Zähler. In der Java-Dokumentation heißt es: „Wenn Threads wiederholt auf diese Weise heruntergezählt werden müssen, verwenden Sie stattdessen eine CyclicBarrier.“ Mit anderen Worten: Wenn Sie einen wiederverwendbaren Zähler benötigen, benötigen Sie eine andere Option:
CyclicBarrier
.
CyclicBarrier
Wie der Name schon sagt,
CyclicBarrier
handelt es sich um eine „wiederverwendbare“ Barriere. Wir müssen die
java.util.concurrent.CyclicBarrier
Klasse importieren. Schauen wir uns ein Beispiel an:
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();
}
}
Wie Sie sehen, führt der Thread die
await
Methode aus, dh er wartet. In diesem Fall sinkt der Barrierewert. Die Barriere gilt als durchbrochen (
barrier.isBroken()
), wenn der Countdown Null erreicht.
reset()
Um die Barriere zurückzusetzen, müssen Sie die Methode aufrufen , die
CountDownLatch
nicht vorhanden ist.
Austauscher
Der nächste Mechanismus ist Exchanger. In diesem Zusammenhang ist ein Exchange ein Synchronisationspunkt, an dem Dinge ausgetauscht oder getauscht werden. Wie zu erwarten ist,
Exchanger
handelt es sich bei an um eine Klasse, die einen Austausch durchführt. Schauen wir uns das einfachste Beispiel an:
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();
}
Hier starten wir zwei Threads. Jeder von ihnen führt die Exchange-Methode aus und wartet darauf, dass der andere Thread ebenfalls die Exchange-Methode ausführt. Dabei tauschen die Threads die übergebenen Argumente aus. Interessant. Erinnert es Sie nicht an etwas? Es erinnert an
SynchronousQueue
, was das Herzstück von ist
CachedThreadPool
. Zur Verdeutlichung hier ein Beispiel:
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");
}
Das Beispiel zeigt, dass beim Start eines neuen Threads dieser wartet, da die Warteschlange leer ist. Und dann stellt der Hauptthread die Zeichenfolge „Nachricht“ in die Warteschlange. Darüber hinaus wird es auch angehalten, bis diese Zeichenfolge aus der Warteschlange empfangen wird. Sie können auch „
SynchronousQueue vs Exchanger “ lesen, um mehr über dieses Thema zu erfahren.
Phaser
Wir haben das Beste zum Schluss aufgehoben –
Phaser
. Wir müssen die
java.util.concurrent.Phaser
Klasse importieren. Schauen wir uns ein einfaches Beispiel an:
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();
}
Das Beispiel zeigt, dass bei Verwendung
Phaser
von die Schranke bricht, wenn die Anzahl der Registrierungen mit der Anzahl der Ankünfte an der Schranke übereinstimmt. Sie können sich besser damit vertraut machen , indem Sie
diesen Artikel von GeeksforGeeksPhaser
lesen .
Zusammenfassung
Wie Sie anhand dieser Beispiele sehen können, gibt es verschiedene Möglichkeiten, Threads zu synchronisieren. Zuvor habe ich versucht, mich an Aspekte des Multithreadings zu erinnern. Ich hoffe, dass die vorherigen Teile dieser Serie hilfreich waren. Manche Leute sagen, dass der Weg zum Multithreading mit dem Buch „Java Concurrency in Practice“ beginnt. Obwohl es bereits 2006 veröffentlicht wurde, sagen die Leute, dass das Buch ziemlich grundlegend und auch heute noch relevant ist. Die Diskussion können Sie beispielsweise hier nachlesen:
Ist „Java Concurrency In Practice“ noch gültig? . Es ist auch nützlich, die Links in der Diskussion zu lesen. Es gibt beispielsweise einen Link zum Buch
The Well-Grounded Java Developer , und wir werden insbesondere Kapitel 4 erwähnen. Moderne Parallelität . Zu diesem Thema gibt es auch eine komplette Rezension:
Ist „Java-Parallelität in der Praxis“ im Zeitalter von Java 8 noch gültig? Dieser Artikel bietet auch Vorschläge, was Sie sonst noch lesen sollten, um dieses Thema wirklich zu verstehen. Danach könnten Sie einen Blick auf ein großartiges Buch wie „
OCA/OCP Java SE 8 Programmer Practice Tests“ werfen . Uns interessiert das zweite Akronym: OCP (Oracle Certified Professional). Tests finden Sie in „Kapitel 20: Java-Parallelität“. Dieses Buch enthält sowohl Fragen als auch Antworten mit Erklärungen. Zum Beispiel:
Viele Leute sagen vielleicht, dass diese Frage ein weiteres Beispiel für das Auswendiglernen von Methoden ist. Einerseits ja. Andererseits könnten Sie diese Frage beantworten, indem Sie sich daran erinnern, dass es sich um
ExecutorService
eine Art „Upgrade“ von handelt
Executor
. Und
Executor
soll lediglich die Art und Weise verbergen, wie Threads erstellt werden, ist jedoch nicht die Hauptmethode für deren Ausführung, d. h. das Starten eines
Runnable
Objekts in einem neuen Thread. Deshalb gibt es keine
execute(Callable)
– denn in fügt
ExecutorService
die
Executor
einfach
submit()
Methoden hinzu, die ein Objekt zurückgeben können
Future
. Natürlich können wir uns eine Liste von Methoden merken, aber es ist viel einfacher, unsere Antwort auf der Grundlage unseres Wissens über die Natur der Klassen selbst zu formulieren. Und hier sind einige zusätzliche Materialien zum Thema:
Besser zusammen: Java und die Thread-Klasse. Teil I – Ausführungsthreads Gemeinsam besser: Java und die Thread-Klasse. Teil II – Synchronisierung Gemeinsam besser: Java und die Thread-Klasse. Teil III – Gemeinsam besser interagieren: Java und die Thread-Klasse. Teil IV – Callable, Future und Freunde Gemeinsam besser: Java und die Thread-Klasse. Teil V – Executor, ThreadPool, Fork/Join
GO TO FULL VERSION