Java Language
Gleichzeitige Programmierung (Threads)
Suche…
Einführung
Gleichzeitiges Rechnen ist eine Form des Rechnens, bei der mehrere Berechnungen gleichzeitig statt sequentiell ausgeführt werden. Die Programmiersprache Java unterstützt die gleichzeitige Programmierung durch die Verwendung von Threads. Auf Objekte und Ressourcen kann von mehreren Threads zugegriffen werden. Jeder Thread kann potenziell auf jedes Objekt im Programm zugreifen, und der Programmierer muss sicherstellen, dass der Lese- und Schreibzugriff auf Objekte ordnungsgemäß zwischen Threads synchronisiert wird.
Bemerkungen
Verwandte Themen zu StackOverflow:
Grundlegendes Multithreading
Wenn Sie viele Aufgaben ausführen müssen und alle diese Aufgaben nicht vom Ergebnis der vorangegangenen Aufgaben abhängig sind, können Sie Multithreading für Ihren Computer verwenden, um alle diese Aufgaben gleichzeitig auszuführen und mehr Prozessoren zu verwenden, sofern Ihr Computer dies kann. Dies kann die Programmausführung beschleunigen, wenn Sie große unabhängige Aufgaben haben.
class CountAndPrint implements Runnable {
private final String name;
CountAndPrint(String name) {
this.name = name;
}
/** This is what a CountAndPrint will do */
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
System.out.println(this.name + ": " + i);
}
}
public static void main(String[] args) {
// Launching 4 parallel threads
for (int i = 1; i <= 4; i++) {
// `start` method will call the `run` method
// of CountAndPrint in another thread
new Thread(new CountAndPrint("Instance " + i)).start();
}
// Doing some others tasks in the main Thread
for (int i = 0; i < 10000; i++) {
System.out.println("Main: " + i);
}
}
}
Der Code der run-Methode der verschiedenen CountAndPrint
Instanzen wird in nicht vorhersagbarer Reihenfolge ausgeführt. Ein Ausschnitt einer Beispielausführung könnte folgendermaßen aussehen:
Instance 4: 1
Instance 2: 1
Instance 4: 2
Instance 1: 1
Instance 1: 2
Main: 1
Instance 4: 3
Main: 2
Instance 3: 1
Instance 4: 4
...
Produzent-Verbraucher
Ein einfaches Beispiel für eine Hersteller-Verbraucher-Problemlösung. Beachten Sie, dass JDK-Klassen ( AtomicBoolean
und BlockingQueue
) für die Synchronisierung verwendet werden, wodurch die Wahrscheinlichkeit verringert wird, dass eine ungültige Lösung erstellt wird. Fragen Sie Javadoc nach verschiedenen Arten von BlockingQueue . Die Wahl einer anderen Implementierung kann das Verhalten dieses Beispiels drastisch ändern (wie DelayQueue oder Priority Queue ).
public class Producer implements Runnable {
private final BlockingQueue<ProducedData> queue;
public Producer(BlockingQueue<ProducedData> queue) {
this.queue = queue;
}
public void run() {
int producedCount = 0;
try {
while (true) {
producedCount++;
//put throws an InterruptedException when the thread is interrupted
queue.put(new ProducedData());
}
} catch (InterruptedException e) {
// the thread has been interrupted: cleanup and exit
producedCount--;
//re-interrupt the thread in case the interrupt flag is needeed higher up
Thread.currentThread().interrupt();
}
System.out.println("Produced " + producedCount + " objects");
}
}
public class Consumer implements Runnable {
private final BlockingQueue<ProducedData> queue;
public Consumer(BlockingQueue<ProducedData> queue) {
this.queue = queue;
}
public void run() {
int consumedCount = 0;
try {
while (true) {
//put throws an InterruptedException when the thread is interrupted
ProducedData data = queue.poll(10, TimeUnit.MILLISECONDS);
// process data
consumedCount++;
}
} catch (InterruptedException e) {
// the thread has been interrupted: cleanup and exit
consumedCount--;
//re-interrupt the thread in case the interrupt flag is needeed higher up
Thread.currentThread().interrupt();
}
System.out.println("Consumed " + consumedCount + " objects");
}
}
public class ProducerConsumerExample {
static class ProducedData {
// empty data object
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<ProducedData> queue = new ArrayBlockingQueue<ProducedData>(1000);
// choice of queue determines the actual behavior: see various BlockingQueue implementations
Thread producer = new Thread(new Producer(queue));
Thread consumer = new Thread(new Consumer(queue));
producer.start();
consumer.start();
Thread.sleep(1000);
producer.interrupt();
Thread.sleep(10);
consumer.interrupt();
}
}
ThreadLocal verwenden
Ein nützliches Werkzeug in Java Concurrency ist ThreadLocal
Damit können Sie eine Variable haben, die für einen bestimmten Thread eindeutig ist. Wenn also derselbe Code in verschiedenen Threads ausgeführt wird, teilen diese Ausführungen den Wert nicht, sondern jeder Thread hat seine eigene Variable, die lokal für den Thread ist .
Dies wird beispielsweise häufig verwendet, um den Kontext (z. B. Berechtigungsinformationen) für die Bearbeitung einer Anforderung in einem Servlet festzulegen. Sie könnten so etwas tun:
private static final ThreadLocal<MyUserContext> contexts = new ThreadLocal<>();
public static MyUserContext getContext() {
return contexts.get(); // get returns the variable unique to this thread
}
public void doGet(...) {
MyUserContext context = magicGetContextFromRequest(request);
contexts.put(context); // save that context to our thread-local - other threads
// making this call don't overwrite ours
try {
// business logic
} finally {
contexts.remove(); // 'ensure' removal of thread-local variable
}
}
Anstatt MyUserContext
an jede einzelne Methode zu übergeben, können Sie MyServlet.getContext()
stattdessen dort verwenden, wo Sie es benötigen. Natürlich führt dies nun eine Variable ein, die dokumentiert werden muss, aber sie ist Thread-sicher, was viele Nachteile bei der Verwendung einer solchen Variablen mit großem Umfang beseitigt.
Der entscheidende Vorteil hierbei ist, dass jeder Thread eine eigene lokale Threadvariable in diesem contexts
. Solange Sie ihn von einem definierten Einstiegspunkt aus verwenden (z. B. die Anforderung, dass jedes Servlet seinen Kontext beibehält, oder indem Sie einen Servlet-Filter hinzufügen), können Sie sich darauf verlassen, dass dieser Kontext vorhanden ist, wenn Sie ihn benötigen.
CountDownLatch
Eine Synchronisationshilfe, mit der ein oder mehrere Threads warten können, bis ein Satz von Vorgängen in anderen Threads abgeschlossen ist.
- Ein
CountDownLatch
wird mit einer bestimmten Anzahl initialisiert. - Die wait-Methoden werden blockiert, bis der aktuelle Zählerstand null ist, weil
countDown()
dercountDown()
-MethodecountDown()
, wonach alle wartenden Threads freigegeben werden und alle nachfolgenden Aufrufe von await sofort zurückgegeben werden. - Dies ist ein einmaliges Phänomen - die Zählung kann nicht zurückgesetzt werden. Wenn Sie eine Version benötigen, die die Zählung zurücksetzt, sollten Sie einen
CyclicBarrier
.
Hauptmethoden:
public void await() throws InterruptedException
Bewirkt, dass der aktuelle Thread wartet, bis der Latch auf null heruntergezählt ist, es sei denn, der Thread ist unterbrochen.
public void countDown()
Verringert die Anzahl der Latches und gibt alle wartenden Threads frei, wenn die Anzahl null erreicht.
Beispiel:
import java.util.concurrent.*;
class DoSomethingInAThread implements Runnable {
CountDownLatch latch;
public DoSomethingInAThread(CountDownLatch latch) {
this.latch = latch;
}
public void run() {
try {
System.out.println("Do some thing");
latch.countDown();
} catch(Exception err) {
err.printStackTrace();
}
}
}
public class CountDownLatchDemo {
public static void main(String[] args) {
try {
int numberOfThreads = 5;
if (args.length < 1) {
System.out.println("Usage: java CountDownLatchDemo numberOfThreads");
return;
}
try {
numberOfThreads = Integer.parseInt(args[0]);
} catch(NumberFormatException ne) {
}
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int n = 0; n < numberOfThreads; n++) {
Thread t = new Thread(new DoSomethingInAThread(latch));
t.start();
}
latch.await();
System.out.println("In Main thread after completion of " + numberOfThreads + " threads");
} catch(Exception err) {
err.printStackTrace();
}
}
}
Ausgabe:
java CountDownLatchDemo 5
Do some thing
Do some thing
Do some thing
Do some thing
Do some thing
In Main thread after completion of 5 threads
Erläuterung:
-
CountDownLatch
wird mit einem Zähler von 5 im Hauptthread initialisiert - Der Hauptthread wartet mit der Methode
await()
. - Fünf Instanzen von
DoSomethingInAThread
wurden erstellt. Bei jeder Instanz wurde der Zähler mit dercountDown()
Methode dekrementiert. - Sobald der Zähler Null wird, wird der Hauptthread fortgesetzt
Synchronisation
In Java gibt es einen eingebauten Sperrmechanismus auf Sprachebene: Der synchronized
Block, der ein beliebiges Java-Objekt als intrinsische Sperre verwenden kann (dh jedem Java-Objekt kann ein Monitor zugeordnet sein).
Intrinsische Sperren verleihen Gruppen von Aussagen Atomizität. Um zu verstehen, was das für uns bedeutet, werfen wir einen Blick auf ein Beispiel, in dem eine synchronized
nützlich ist:
private static int t = 0;
private static Object mutex = new Object();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(400); // The high thread count is for demonstration purposes.
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
synchronized (mutex) {
t++;
System.out.println(MessageFormat.format("t: {0}", t));
}
});
}
executorService.shutdown();
}
In diesem Fall wären ohne den synchronized
Block mehrere Parallelitätsprobleme aufgetreten. Der erste wäre mit dem Post-Inkrement-Operator (er ist nicht in sich atomar) und der zweite wäre, dass wir den Wert von t beobachten würden, nachdem eine beliebige Anzahl anderer Threads die Möglichkeit hatte, ihn zu ändern. Da wir jedoch eine intrinsische Sperre erworben haben, gibt es hier keine Race-Bedingungen und die Ausgabe enthält Zahlen von 1 bis 100 in ihrer normalen Reihenfolge.
Intrinsische Sperren in Java sind Mutexe (dh gegenseitige Ausführungssperren). Gegenseitige Ausführung bedeutet, dass, wenn ein Thread die Sperre erworben hat, der zweite gezwungen ist, auf den ersten Thread zu warten, bevor er freigegeben wird, bevor er die Sperre für sich selbst erwerben kann. Hinweis: Eine Operation, die den Thread möglicherweise in den Wartezustand versetzt, wird als Sperroperation bezeichnet . Daher ist der Erwerb einer Sperre eine Sperroperation.
Intrinsische Sperren in Java sind wiedereintrittsfähig . Das heißt, wenn ein Thread versucht, eine Sperre zu erwerben, die er bereits besitzt, wird er nicht blockiert und er wird sie erfolgreich abrufen. Beispielsweise wird der folgende Code beim Aufruf nicht blockiert:
public void bar(){
synchronized(this){
...
}
}
public void foo(){
synchronized(this){
bar();
}
}
Neben synchronized
Blöcken gibt es auch synchronized
Methoden.
Die folgenden Codeblöcke sind praktisch gleichwertig (obwohl der Bytecode anders zu sein scheint):
synchronized
Blockthis
:public void foo() { synchronized(this) { doStuff(); } }
synchronized
Methode:public synchronized void foo() { doStuff(); }
Ebenso für static
Methoden:
class MyClass {
...
public static void bar() {
synchronized(MyClass.class) {
doSomeOtherStuff();
}
}
}
hat den gleichen Effekt wie dieser:
class MyClass {
...
public static synchronized void bar() {
doSomeOtherStuff();
}
}
Atomare Operationen
Eine atomare Operation ist eine Operation, die "alle gleichzeitig" ausgeführt wird, ohne dass andere Threads während der Ausführung der atomaren Operation den Zustand beobachten oder ändern können.
Betrachten wir ein schlechtes Beispiel .
private static int t = 0;
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(400); // The high thread count is for demonstration purposes.
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
t++;
System.out.println(MessageFormat.format("t: {0}", t));
});
}
executorService.shutdown();
}
In diesem Fall gibt es zwei Probleme. Das erste Problem ist, dass der Post-Inkrement-Operator nicht atomar ist. Es besteht aus mehreren Operationen: Holen Sie den Wert, addieren Sie 1 zum Wert, setzen Sie den Wert. Wenn wir also das Beispiel ausführen, sehen wir wahrscheinlich nicht t: 100
in der Ausgabe - zwei Threads können gleichzeitig den Wert abrufen, erhöhen und einstellen: Nehmen wir an, der Wert von t ist 10 und zwei Threads erhöhen t. Beide Threads setzen den Wert von t auf 11, da der zweite Thread den Wert von t feststellt, bevor der erste Thread das Inkrementieren beendet hat.
Die zweite Frage ist, wie wir t beobachten. Wenn wir den Wert von t drucken, wurde der Wert nach der Inkrementierungsoperation dieses Threads möglicherweise bereits von einem anderen Thread geändert.
Um diese Probleme zu beheben, verwenden wir den java.util.concurrent.atomic.AtomicInteger
, der viele atomare Operationen enthält.
private static AtomicInteger t = new AtomicInteger(0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(400); // The high thread count is for demonstration purposes.
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
int currentT = t.incrementAndGet();
System.out.println(MessageFormat.format("t: {0}", currentT));
});
}
executorService.shutdown();
}
Die incrementAndGet
Methode von AtomicInteger
inkrementiert den neuen Wert atomar und gibt diesen zurück, wodurch die vorherige AtomicInteger
beseitigt wird. Bitte beachten Sie, dass in diesem Beispiel die Zeilen immer noch nicht in Ordnung sind, da wir uns nicht bemühen, die println
Aufrufe zu sequenzieren. Dies fällt nicht in den Anwendungsbereich dieses Beispiels, da dies eine Synchronisation erfordern würde AtomicInteger
, um Race-Bedingungen bezüglich des Zustands zu beseitigen.
Ein grundlegendes Deadlock-System erstellen
Ein Deadlock tritt auf, wenn zwei konkurrierende Aktionen darauf warten, dass die andere beendet wird. In Java ist jedem Objekt eine Sperre zugeordnet. Um gleichzeitige Änderungen zu vermeiden, die von mehreren Threads für ein einzelnes Objekt ausgeführt werden, können wir synchronized
Schlüsselwörter verwenden, aber alles kostet einen Preis. Die falsche Verwendung von synchronized
Schlüsselwörtern kann dazu führen, dass Systeme stecken bleiben, die als Deadlock-System bezeichnet werden.
Angenommen, es gibt 2 Threads, die in einer Instanz arbeiten, lassen Sie uns Threads als First und Second aufrufen, und sagen wir, wir haben 2 Ressourcen R1 und R2. First erwirbt R1 und benötigt außerdem R2 für die Fertigstellung, während Second R2 beschafft und R1 für die Fertigstellung benötigt.
also zum Zeitpunkt t = 0 sagen,
First hat R1 und Second hat R2. Jetzt wartet First auf R2, während Second auf R1 wartet. Dieses Warten ist unbegrenzt und führt zu einem Deadlock.
public class Example2 {
public static void main(String[] args) throws InterruptedException {
final DeadLock dl = new DeadLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
dl.methodA();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
dl.method2();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
t1.setName("First");
t2.setName("Second");
t1.start();
t2.start();
}
}
class DeadLock {
Object mLock1 = new Object();
Object mLock2 = new Object();
public void methodA() {
System.out.println("methodA wait for mLock1 " + Thread.currentThread().getName());
synchronized (mLock1) {
System.out.println("methodA mLock1 acquired " + Thread.currentThread().getName());
try {
Thread.sleep(100);
method2();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void method2() throws InterruptedException {
System.out.println("method2 wait for mLock2 " + Thread.currentThread().getName());
synchronized (mLock2) {
System.out.println("method2 mLock2 acquired " + Thread.currentThread().getName());
Thread.sleep(100);
method3();
}
}
public void method3() throws InterruptedException {
System.out.println("method3 mLock1 "+ Thread.currentThread().getName());
synchronized (mLock1) {
System.out.println("method3 mLock1 acquired " + Thread.currentThread().getName());
}
}
}
Ausgabe dieses Programms:
methodA wait for mLock1 First
method2 wait for mLock2 Second
method2 mLock2 acquired Second
methodA mLock1 acquired First
method3 mLock1 Second
method2 wait for mLock2 First
Ausführung unterbrechen
Thread.sleep
bewirkt, dass der aktuelle Thread die Ausführung für einen bestimmten Zeitraum Thread.sleep
. Dies ist ein effizientes Mittel, um den anderen Threads einer Anwendung oder anderen Anwendungen, die auf einem Computersystem ausgeführt werden, Prozessorzeit zur Verfügung zu stellen. Die Thread-Klasse enthält zwei überladene sleep
Methoden.
Eine, die die Schlafzeit in Millisekunden angibt
public static void sleep(long millis) throws InterruptedException
Eine, die die Schlafzeit in Nanosekunden angibt
public static void sleep(long millis, int nanos)
Die Ausführung wird für 1 Sekunde angehalten
Thread.sleep(1000);
Es ist wichtig zu beachten, dass dies ein Hinweis auf den Scheduler des Kernels des Betriebssystems ist. Dies muss nicht unbedingt genau sein, und einige Implementierungen berücksichtigen nicht einmal den Nanosekundenparameter (möglicherweise wird auf die nächste Millisekunde gerundet).
Es wird empfohlen, einen Aufruf von Thread.sleep
in try / catch Thread.sleep
und InterruptedException
Thread.sleep
.
Visualisierung von Lese- / Schreibbarrieren bei gleichzeitiger Verwendung von synchronisiert / flüchtig
Wir wissen, dass wir ein synchronized
Schlüsselwort verwenden sollten, um die Ausführung einer Methode oder eines Blocks exklusiv zu machen. Wenigen von uns ist jedoch ein wichtiger Aspekt der Verwendung synchronized
und volatile
Schlüsselwörter nicht bewusst: Abgesehen von der Bildung einer Einheit aus Code-Einheiten bietet sie auch eine Lese- / Schreibbarriere . Was ist diese Lese- / Schreibsperre? Lassen Sie uns dies an einem Beispiel besprechen:
class Counter {
private Integer count = 10;
public synchronized void incrementCount() {
count++;
}
public Integer getCount() {
return count;
}
}
Nehmen wir an, ein Thread A ruft zuerst incrementCount()
dann ruft ein anderer Thread B getCount()
. In diesem Szenario gibt es keine Garantie, dass B einen aktualisierten count
erhält. Es kann immer noch count
als 10
, auch ist es möglich, dass nie aktualisierte Werte für die count
angezeigt werden.
Um dieses Verhalten zu verstehen, müssen wir verstehen, wie das Java-Speichermodell in die Hardwarearchitektur integriert wird. In Java hat jeder Thread einen eigenen Threadstapel. Dieser Stack enthält: Method Call Stack und lokale Variable, die in diesem Thread erstellt wurden. In einem Multi-Core-System ist es durchaus möglich, dass zwei Threads gleichzeitig in separaten Cores laufen. In einem solchen Szenario ist es möglich, dass ein Teil des Stapels eines Threads im Register / Cache eines Kerns liegt. Wenn innerhalb eines Threads auf ein Objekt mit synchronized
(oder volatile
) Schlüsselwörtern zugegriffen wird, synchronized
dieser Thread nach dem synchronized
Block seine lokale Kopie dieser Variablen mit dem Hauptspeicher. Dadurch wird eine Lese- / Schreibsperre erstellt und sichergestellt, dass der Thread den neuesten Wert dieses Objekts erkennt.
Da Thread B jedoch keinen synchronisierten Zugriff für count
, verweist er in diesem Fall möglicherweise auf den im Register gespeicherten Wert von count
und kann niemals Aktualisierungen von Thread A getCount()
synchronisiert.
public synchronized Integer getCount() {
return count;
}
Wenn nun Faden A mit der Aktualisierung erfolgt count
entriegelt es Counter
Beispiel zugleich Schreibbarriere erzeugt und spült alle innerhalb dieses Blocks in den Hauptspeicher erfolgen Änderungen. Wenn Thread B eine Sperrung für dieselbe Instanz von Counter
erlangt, tritt er auf ähnliche Weise in die Lesebarriere ein, liest den count
aus dem Hauptspeicher und sieht alle Aktualisierungen.
Der gleiche Sichtbarkeitseffekt gilt auch für volatile
Lesen / Schreiben. Alle vor dem Schreiben in volatile
Variablen aktualisierten Variablen werden in den Hauptspeicher geschrieben, und alle Lesevorgänge nach dem Lesen von volatile
Variablen werden aus dem Hauptspeicher stammen.
Erstellen einer java.lang.Thread-Instanz
Es gibt zwei Hauptansätze zum Erstellen eines Threads in Java. Im Grunde ist das Erstellen eines Threads so einfach wie das Schreiben des Codes, der darin ausgeführt wird. Die beiden Ansätze unterscheiden sich darin, wo Sie diesen Code definieren.
In Java wird ein Thread durch ein Objekt dargestellt - eine Instanz von java.lang.Thread oder dessen Unterklasse. Der erste Ansatz besteht darin, diese Unterklasse zu erstellen und die run () -Methode zu überschreiben.
Hinweis : Ich verwende Thread , um auf die Klasse java.lang.Thread zu verweisen, und Thread , um auf das logische Konzept von Threads zu verweisen.
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Thread running!");
}
}
}
Da wir den auszuführenden Code bereits definiert haben, kann der Thread einfach erstellt werden als:
MyThread t = new MyThread();
Die Thread- Klasse enthält auch einen Konstruktor, der eine Zeichenfolge akzeptiert, die als Name des Threads verwendet wird. Dies kann besonders hilfreich sein, wenn Sie ein Multi-Thread-Programm debuggen.
class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Thread running! ");
}
}
}
MyThread t = new MyThread("Greeting Producer");
Der zweite Ansatz besteht darin, den Code mit java.lang.Runnable und seiner einzigen Methode run () zu definieren . Die Thread- Klasse ermöglicht es Ihnen dann, diese Methode in einem separaten Thread auszuführen. Um dies zu erreichen, erstellen Sie den Thread mit einem Konstruktor, der eine Instanz der ausführbaren Schnittstelle akzeptiert.
Thread t = new Thread(aRunnable);
Dies kann sehr effektiv sein, wenn es mit Lambdas oder Methodenreferenzen kombiniert wird (nur Java 8):
Thread t = new Thread(operator::hardWork);
Sie können auch den Namen des Threads angeben.
Thread t = new Thread(operator::hardWork, "Pi operator");
Praktisch können Sie beide Ansätze ohne Sorgen verwenden. Die allgemeine Weisheit sagt jedoch, letzteres zu verwenden.
Für jeden der vier genannten Konstruktoren gibt es auch eine Alternative, die eine Instanz von java.lang.ThreadGroup als ersten Parameter akzeptiert.
ThreadGroup tg = new ThreadGroup("Operators");
Thread t = new Thread(tg, operator::hardWork, "PI operator");
Die ThreadGroup repräsentiert eine Gruppe von Threads. Sie können nur hinzufügen Thema zu einem Thread mit einem Thema ‚s - Konstruktor. Die ThreadGroup kann dann verwendet werden, um alle Threads zusammen zu verwalten, und der Thread kann Informationen von seiner ThreadGroup erhalten .
Um dies zu sumarisieren, kann der Thread mit einem dieser öffentlichen Konstruktoren erstellt werden:
Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target, String name)
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
Mit dem letzten können wir die gewünschte Stapelgröße für den neuen Thread definieren.
Häufig leidet die Lesbarkeit des Codes, wenn viele Threads mit denselben Eigenschaften oder mit demselben Muster erstellt und konfiguriert werden. Dann kann java.util.concurrent.ThreadFactory verwendet werden. Mit dieser Schnittstelle können Sie die Prozedur zum Erstellen des Threads durch das Factory-Pattern und die einzige Methode newThread (Runnable) kapseln .
class WorkerFactory implements ThreadFactory {
private int id = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Worker " + id++);
}
}
Thread-Unterbrechung / Stoppen von Threads
Jeder Java-Thread hat ein Interrupt-Flag, das anfangs falsch ist. Das Unterbrechen eines Threads ist im Wesentlichen nichts anderes als das Setzen des Flags auf true. Der Code, der in diesem Thread ausgeführt wird, kann die Flagge gelegentlich überprüfen und darauf reagieren. Der Code kann ihn auch komplett ignorieren. Aber warum sollte jeder Thread eine solche Flagge haben? Eine boolesche Fahne in einem Thread zu haben, ist etwas, was wir uns selbst organisieren können, wenn wir es brauchen. Nun, es gibt Methoden, die sich auf besondere Weise verhalten, wenn der Thread, auf dem sie laufen, unterbrochen wird. Diese Methoden werden Sperrmethoden genannt. Dies sind Methoden, die den Thread in den Status WAITING oder TIMED_WAITING setzen. Wenn sich ein Thread in diesem Zustand befindet, wird bei Unterbrechung eine InterruptedException für den unterbrochenen Thread ausgelöst, anstatt dass das Interrupt-Flag auf true gesetzt ist, und der Thread wird erneut zu RUNNABLE. Code, der eine Blockierungsmethode aufruft, muss die InterruptedException behandeln, da es sich um eine geprüfte Ausnahme handelt. Ein Interrupt kann also und damit auch der Name eine WAIT unterbrechen und effektiv beenden. Beachten Sie, dass nicht alle Methoden, die irgendwie warten (z. B. das Blockieren von E / A), auf diese Weise auf eine Unterbrechung reagieren, da sie den Thread nicht in einen Wartezustand versetzen. Ein Thread, dessen Interrupt-Flag gesetzt ist und in eine Sperrmethode eintritt (dh versucht, in einen Wartezustand zu gelangen), löst sofort eine InterruptedException aus und das Interrupt-Flag wird gelöscht.
Mit Ausnahme dieser Mechanismen ordnet Java der Unterbrechung keine besondere semantische Bedeutung zu. Der Code kann einen Interrupt beliebig interpretieren. In den meisten Fällen wird eine Unterbrechung verwendet, um einem Thread zu signalisieren, dass er so schnell wie möglich nicht mehr läuft. Wie sich jedoch aus dem Vorstehenden ergibt, ist es Sache des Codes in diesem Thread, auf diese Unterbrechung entsprechend zu reagieren, um die Ausführung zu stoppen. Das Stoppen eines Threads ist eine Kollaboration. Wenn ein Thread unterbrochen wird, kann sein laufender Code mehrere Ebenen tief im Stacktrace liegen. Die meisten Codes rufen keine Blockierungsmethode auf und werden rechtzeitig beendet, um das Stoppen des Threads nicht unnötig zu verzögern. Der Code, der hauptsächlich darauf angesprochen werden soll, auf eine Unterbrechung zu reagieren, ist Code, der sich in einer Schleife befindet, in der Aufgaben ausgeführt werden, bis keine mehr vorhanden ist oder bis ein Flag gesetzt ist, das signalisiert, dass die Schleife angehalten wird. Schleifen, die möglicherweise unendliche Aufgaben bearbeiten (dh sie laufen grundsätzlich weiter), sollten das Interrupt-Flag prüfen, um die Schleife zu verlassen. Bei endlichen Schleifen kann die Semantik vorschreiben, dass alle Aufgaben vor dem Beenden abgeschlossen werden müssen, oder es kann angebracht sein, einige Aufgaben nicht bearbeitet zu lassen. Code, der blockierende Methoden aufruft, wird gezwungen, mit der InterruptedException umzugehen. Wenn überhaupt semantisch möglich, kann die InterruptedException einfach propagiert und zum Auslösen deklariert werden. Als solches wird es selbst gegenüber seinen Anrufern zu einer Sperrmethode. Wenn es die Ausnahme nicht weitergeben kann, sollte es zumindest das unterbrochene Flag setzen, sodass Anrufer oberhalb des Stacks auch wissen, dass der Thread unterbrochen wurde. In einigen Fällen muss die Methode unabhängig von der InterruptedException weiter warten. In diesem Fall muss das Setzen des unterbrochenen Flags so lange verzögert werden, bis das Warten beendet ist. Dazu muss möglicherweise eine lokale Variable festgelegt werden, die vor dem Verlassen der Methode überprüft werden muss Dann unterbrechen Sie den Thread.
Beispiele:
Beispiel für Code, der die Bearbeitung von Aufgaben nach einer Unterbrechung stoppt
class TaskHandler implements Runnable {
private final BlockingQueue<Task> queue;
TaskHandler(BlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) { // check for interrupt flag, exit loop when interrupted
try {
Task task = queue.take(); // blocking call, responsive to interruption
handle(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // cannot throw InterruptedException (due to Runnable interface restriction) so indicating interruption by setting the flag
}
}
}
private void handle(Task task) {
// actual handling
}
}
Beispiel für Code, der das Setzen des Interrupt-Flags verzögert, bis der Vorgang abgeschlossen ist:
class MustFinishHandler implements Runnable {
private final BlockingQueue<Task> queue;
MustFinishHandler(BlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
boolean shouldInterrupt = false;
while (true) {
try {
Task task = queue.take();
if (task.isEndOfTasks()) {
if (shouldInterrupt) {
Thread.currentThread().interrupt();
}
return;
}
handle(task);
} catch (InterruptedException e) {
shouldInterrupt = true; // must finish, remember to set interrupt flag when we're done
}
}
}
private void handle(Task task) {
// actual handling
}
}
Beispiel für Code, der eine feste Liste von Aufgaben enthält, bei Unterbrechung jedoch vorzeitig beendet wird
class GetAsFarAsPossible implements Runnable {
private final List<Task> tasks = new ArrayList<>();
@Override
public void run() {
for (Task task : tasks) {
if (Thread.currentThread().isInterrupted()) {
return;
}
handle(task);
}
}
private void handle(Task task) {
// actual handling
}
}
Beispiel für mehrere Produzenten / Konsumenten mit gemeinsam genutzter globaler Warteschlange
Der folgende Code zeigt mehrere Producer / Consumer-Programme. Sowohl Producer- als auch Consumer-Threads verwenden dieselbe globale Warteschlange.
import java.util.concurrent.*;
import java.util.Random;
public class ProducerConsumerWithES {
public static void main(String args[]) {
BlockingQueue<Integer> sharedQueue = new LinkedBlockingQueue<Integer>();
ExecutorService pes = Executors.newFixedThreadPool(2);
ExecutorService ces = Executors.newFixedThreadPool(2);
pes.submit(new Producer(sharedQueue, 1));
pes.submit(new Producer(sharedQueue, 2));
ces.submit(new Consumer(sharedQueue, 1));
ces.submit(new Consumer(sharedQueue, 2));
pes.shutdown();
ces.shutdown();
}
}
/* Different producers produces a stream of integers continuously to a shared queue,
which is shared between all Producers and consumers */
class Producer implements Runnable {
private final BlockingQueue<Integer> sharedQueue;
private int threadNo;
private Random random = new Random();
public Producer(BlockingQueue<Integer> sharedQueue,int threadNo) {
this.threadNo = threadNo;
this.sharedQueue = sharedQueue;
}
@Override
public void run() {
// Producer produces a continuous stream of numbers for every 200 milli seconds
while (true) {
try {
int number = random.nextInt(1000);
System.out.println("Produced:" + number + ":by thread:"+ threadNo);
sharedQueue.put(number);
Thread.sleep(200);
} catch (Exception err) {
err.printStackTrace();
}
}
}
}
/* Different consumers consume data from shared queue, which is shared by both producer and consumer threads */
class Consumer implements Runnable {
private final BlockingQueue<Integer> sharedQueue;
private int threadNo;
public Consumer (BlockingQueue<Integer> sharedQueue,int threadNo) {
this.sharedQueue = sharedQueue;
this.threadNo = threadNo;
}
@Override
public void run() {
// Consumer consumes numbers generated from Producer threads continuously
while(true){
try {
int num = sharedQueue.take();
System.out.println("Consumed: "+ num + ":by thread:"+threadNo);
} catch (Exception err) {
err.printStackTrace();
}
}
}
}
Ausgabe:
Produced:69:by thread:2
Produced:553:by thread:1
Consumed: 69:by thread:1
Consumed: 553:by thread:2
Produced:41:by thread:2
Produced:796:by thread:1
Consumed: 41:by thread:1
Consumed: 796:by thread:2
Produced:728:by thread:2
Consumed: 728:by thread:1
und so weiter ................
Erläuterung:
-
sharedQueue
, einLinkedBlockingQueue
wird von allen Producer- und Consumer-Threads gemeinsam genutzt. - Produzententhreads erzeugen alle 200
sharedQueue
fortlaufend eine ganze Zahl undsharedQueue
sie ansharedQueue
-
Consumer
Thread verbraucht fortlaufend Ganzzahlen aussharedQueue
. - Dieses Programm wird ohne explizite
synchronized
oderLock
Konstrukte implementiert. BlockingQueue ist der Schlüssel, um dies zu erreichen.
BlockingQueue-Implementierungen sind hauptsächlich für die Verwendung in Warteschlangen von Produzenten und Verbrauchern vorgesehen.
BlockingQueue-Implementierungen sind Thread-sicher. Alle Warteschlangenmethoden erzielen ihre Auswirkungen atomar durch interne Sperren oder andere Formen der Parallelitätssteuerung.
Exklusiver Schreib- / gleichzeitiger Lesezugriff
Es ist manchmal erforderlich, dass ein Prozess die gleichen "Daten" gleichzeitig schreibt und liest.
Die ReadWriteLock
Schnittstelle und ihre ReentrantReadWriteLock
Implementierung ermöglicht ein Zugriffsmuster, das wie folgt beschrieben werden kann:
- Es können beliebig viele gleichzeitige Leser der Daten vorhanden sein. Wenn mindestens ein Leserzugriff gewährt wird, ist kein Schreibzugriff möglich.
- Es kann höchstens einen einzigen Schreiber für die Daten geben. Wenn ein Schreiberzugriff gewährt wird, kann kein Leser auf die Daten zugreifen.
Eine Implementierung könnte folgendermaßen aussehen:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Sample {
// Our lock. The constructor allows a "fairness" setting, which guarantees the chronology of lock attributions.
protected static final ReadWriteLock RW_LOCK = new ReentrantReadWriteLock();
// This is a typical data that needs to be protected for concurrent access
protected static int data = 0;
/** This will write to the data, in an exclusive access */
public static void writeToData() {
RW_LOCK.writeLock().lock();
try {
data++;
} finally {
RW_LOCK.writeLock().unlock();
}
}
public static int readData() {
RW_LOCK.readLock().lock();
try {
return data;
} finally {
RW_LOCK.readLock().unlock();
}
}
}
ANMERKUNG 1 : Dieser genaue Anwendungsfall hat eine sauberere Lösung, die AtomicInteger
hier beschriebene Zugriffsmuster funktioniert jedoch unabhängig von der Tatsache, dass Daten hier eine Ganzzahl sind, die als atomische Variante gilt.
ANMERKUNG 2 : Die Sperre für den Leseteil ist wirklich erforderlich, obwohl sie für den Gelegenheitsleser nicht so aussieht. Wenn Sie sich nicht auf der Leserseite verriegeln, kann eine beliebige Anzahl von Dingen schief gehen, unter anderem:
- Es ist nicht garantiert, dass die Schreibvorgänge der Grundwerte auf allen JVMs atomar sind, sodass der Leser z. B. nur 32 Bit eines 64-Bit-Schreibvorgangs sehen könnte, wenn
data
einen 64-Bit-langen Typ haben - Die Sichtbarkeit des Schreibvorgangs von einem Thread, der ihn nicht ausgeführt hat, wird von der JVM nur garantiert, wenn die Beziehung Vor dem Schreibvorgang und dem Lesevorgang festgelegt wird. Diese Beziehung wird hergestellt, wenn sowohl Leser als auch Schreiber ihre jeweiligen Sperren verwenden, jedoch nicht anders
Falls eine höhere Leistung erforderlich ist, ist unter bestimmten Nutzungsarten ein schnellerer StampedLock
verfügbar, der als StampedLock
wird, der unter anderem einen optimistischen StampedLock
implementiert. Diese Sperre unterscheidet sich stark von ReadWriteLock
, und dieses Beispiel ist nicht transponierbar.
Lauffähiges Objekt
Die Runnable
Schnittstelle definiert eine einzige Methode, run()
, die den im Thread ausgeführten Code enthalten soll.
Das Runnable
Objekt wird an den Thread
Konstruktor übergeben. Und die start()
-Methode von Thread wird aufgerufen.
Beispiel
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from a thread");
}
public static void main(String[] args) {
new Thread(new HelloRunnable()).start();
}
}
Beispiel in Java8:
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello world");
new Thread(r).start();
}
Runable vs Thread-Unterklasse
Die Verwendung eines Runnable
Objekts ist allgemeiner, da das Runnable
Objekt eine andere Klasse als Thread
Runnable
kann.
Thread
Unterklassen sind einfacher in einfachen Anwendungen zu verwenden, sind jedoch durch die Tatsache eingeschränkt, dass Ihre Task-Klasse von Thread
.
Ein Runnable
Objekt kann auf die Runnable
Thread-Verwaltungs-APIs Runnable
werden.
Semaphor
Ein Semaphor ist ein übergeordneter Synchronisierer, der eine Reihe von Genehmigungen enthält , die von Threads erworben und freigegeben werden können. Ein Semaphor kann man sich als Zähler für Genehmigungen vorstellen, der beim Erwerb eines Threads dekrementiert und beim Freigeben eines Threads inkrementiert wird. Wenn die Anzahl der Genehmigungen 0
wenn ein Thread versucht, zuzugreifen, wird der Thread blockiert, bis eine Genehmigung verfügbar gemacht wird (oder bis der Thread unterbrochen wird).
Ein Semaphor wird wie folgt initialisiert:
Semaphore semaphore = new Semaphore(1); // The int value being the number of permits
Der Semaphor-Konstruktor akzeptiert einen zusätzlichen booleschen Parameter für Fairness. Wenn false festgelegt ist, gibt diese Klasse keine Garantie für die Reihenfolge ab, in der Threads Genehmigungen erhalten. Wenn Fairness auf true gesetzt ist, garantiert das Semaphor, dass Threads, die eine der Erfassungsmethoden aufrufen, ausgewählt werden, um Genehmigungen in der Reihenfolge zu erhalten, in der ihr Aufruf dieser Methoden verarbeitet wurde. Es wird auf folgende Weise erklärt:
Semaphore semaphore = new Semaphore(1, true);
Betrachten wir nun ein Beispiel aus Javadocs, in dem Semaphore verwendet wird, um den Zugriff auf einen Pool von Elementen zu steuern. In diesem Beispiel wird ein Semaphor verwendet, um Blockierungsfunktionen bereitzustellen, um sicherzustellen, dass beim getItem()
immer Elemente abgerufen werden.
class Pool {
/*
* Note that this DOES NOT bound the amount that may be released!
* This is only a starting value for the Semaphore and has no other
* significant meaning UNLESS you enforce this inside of the
* getNextAvailableItem() and markAsUnused() methods
*/
private static final int MAX_AVAILABLE = 100;
private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
/**
* Obtains the next available item and reduces the permit count by 1.
* If there are no items available, block.
*/
public Object getItem() throws InterruptedException {
available.acquire();
return getNextAvailableItem();
}
/**
* Puts the item into the pool and add 1 permit.
*/
public void putItem(Object x) {
if (markAsUnused(x))
available.release();
}
private Object getNextAvailableItem() {
// Implementation
}
private boolean markAsUnused(Object o) {
// Implementation
}
}
Fügen Sie mit einem Threadpool zwei `int`-Arrays hinzu
Ein Threadpool hat eine Warteschlange mit Tasks, von denen jede auf einem dieser Threads ausgeführt wird.
Das folgende Beispiel zeigt, wie Sie mithilfe eines Threadpools zwei int
Arrays hinzufügen.
int[] firstArray = { 2, 4, 6, 8 };
int[] secondArray = { 1, 3, 5, 7 };
int[] result = { 0, 0, 0, 0 };
ExecutorService pool = Executors.newCachedThreadPool();
// Setup the ThreadPool:
// for each element in the array, submit a worker to the pool that adds elements
for (int i = 0; i < result.length; i++) {
final int worker = i;
pool.submit(() -> result[worker] = firstArray[worker] + secondArray[worker] );
}
// Wait for all Workers to finish:
try {
// execute all submitted tasks
pool.shutdown();
// waits until all workers finish, or the timeout ends
pool.awaitTermination(12, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
pool.shutdownNow(); //kill thread
}
System.out.println(Arrays.toString(result));
Anmerkungen:
Dieses Beispiel ist rein illustrativ. In der Praxis wird es keine Beschleunigung geben, wenn Sie Threads für eine so kleine Aufgabe verwenden. Eine Verlangsamung ist wahrscheinlich, da der Aufwand für die Erstellung und Zeitplanung von Aufgaben die für die Ausführung einer Aufgabe benötigte Zeit überschreitet.
Wenn Sie Java 7 oder eine frühere Version verwendet haben, würden Sie anstelle von Lambdas anonyme Klassen verwenden, um die Aufgaben zu implementieren.
Status aller Threads abrufen, die von Ihrem Programm gestartet wurden, ausgenommen System-Threads
Code-Auszug:
import java.util.Set;
public class ThreadStatus {
public static void main(String args[]) throws Exception {
for (int i = 0; i < 5; i++){
Thread t = new Thread(new MyThread());
t.setName("MyThread:" + i);
t.start();
}
int threadCount = 0;
Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
for (Thread t : threadSet) {
if (t.getThreadGroup() == Thread.currentThread().getThreadGroup()) {
System.out.println("Thread :" + t + ":" + "state:" + t.getState());
++threadCount;
}
}
System.out.println("Thread count started by Main thread:" + threadCount);
}
}
class MyThread implements Runnable {
public void run() {
try {
Thread.sleep(2000);
} catch(Exception err) {
err.printStackTrace();
}
}
}
Ausgabe:
Thread :Thread[MyThread:1,5,main]:state:TIMED_WAITING
Thread :Thread[MyThread:3,5,main]:state:TIMED_WAITING
Thread :Thread[main,5,main]:state:RUNNABLE
Thread :Thread[MyThread:4,5,main]:state:TIMED_WAITING
Thread :Thread[MyThread:0,5,main]:state:TIMED_WAITING
Thread :Thread[MyThread:2,5,main]:state:TIMED_WAITING
Thread count started by Main thread:6
Erläuterung:
Thread.getAllStackTraces().keySet()
gibt alle Thread
einschließlich Anwendungsthreads und Systemthreads zurück. Wenn Sie nur am Status der Threads interessiert sind, die von Ihrer Anwendung gestartet wurden, iterieren Sie den Thread
Satz, indem Sie die Thread-Gruppe eines bestimmten Threads mit Ihrem Hauptprogramm- Thread
überprüfen.
Ohne obige ThreadGroup-Bedingung gibt das Programm den Status unter System Threads zurück:
Reference Handler
Signal Dispatcher
Attach Listener
Finalizer
Callable und Future
Runnable
bietet zwar die Möglichkeit, Code in einem anderen Thread auszuführen, hat jedoch die Einschränkung, dass er kein Ergebnis aus der Ausführung zurückgeben kann. Die einzige Möglichkeit, einen Rückgabewert aus der Ausführung eines Runnable
besteht darin, das Ergebnis einer Variablen zuzuweisen, auf die in einem Bereich außerhalb des Runnable
.
Callable
wurde in Java 5 als Peer to Runnable
. Callable
ist im Wesentlichen der gleiche , außer es einen hat call
- Methode statt run
. Die call
hat die zusätzliche Fähigkeit, ein Ergebnis zurückzugeben, und darf auch geprüfte Ausnahmen auslösen.
Das Ergebnis einer Callable-Aufgabe kann über eine Zukunft abgerufen werden
Future
kann als ein Container von Sorten betrachtet werden, der das Ergebnis der Callable
Berechnung enthält. Die Berechnung der aufrufbaren Daten kann in einem anderen Thread fortgesetzt werden. Jeder Versuch, das Ergebnis einer Future
wird blockiert und liefert das Ergebnis nur, wenn es verfügbar ist.
Aufrufbare Schnittstelle
public interface Callable<V> {
V call() throws Exception;
}
Zukunft
interface Future<V> {
V get();
V get(long timeout, TimeUnit unit);
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
}
Callable und Future Beispiel verwenden:
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newSingleThreadExecutor();
System.out.println("Time At Task Submission : " + new Date());
Future<String> result = es.submit(new ComplexCalculator());
// the call to Future.get() blocks until the result is available.So we are in for about a 10 sec wait now
System.out.println("Result of Complex Calculation is : " + result.get());
System.out.println("Time At the Point of Printing the Result : " + new Date());
}
Unser Callable führt eine langwierige Berechnung aus
public class ComplexCalculator implements Callable<String> {
@Override
public String call() throws Exception {
// just sleep for 10 secs to simulate a lengthy computation
Thread.sleep(10000);
System.out.println("Result after a lengthy 10sec calculation");
return "Complex Result"; // the result
}
}
Ausgabe
Time At Task Submission : Thu Aug 04 15:05:15 EDT 2016
Result after a lengthy 10sec calculation
Result of Complex Calculation is : Complex Result
Time At the Point of Printing the Result : Thu Aug 04 15:05:25 EDT 2016
Andere Operationen sind für Future zulässig
Während get()
die Methode ist, um das tatsächliche Ergebnis zu extrahieren, hat Future eine Provision
-
get(long timeout, TimeUnit unit)
definiert die maximale Zeitdauer, während der der aktuelle Thread auf ein Ergebnis wartet. - So
cancel(mayInterruptIfRunning)
den Task-Aufruf abcancel(mayInterruptIfRunning)
. Das FlagmayInterrupt
zeigt an, dass die Task unterbrochen werden sollte, wenn sie gerade gestartet wurde und gerade ausgeführt wird. - Um zu überprüfen, ob die Aufgabe abgeschlossen / abgeschlossen ist, rufen Sie
isDone()
. - Um zu überprüfen, ob die langwierige Aufgabe abgebrochen wurde,
isCancelled()
.
Sperren als Synchronisationshilfen
Vor der Einführung von Java 5 für gleichzeitige Pakete war das Threading eher untergeordnet. Bei der Einführung dieses Pakets wurden mehrere Programmierhilfen / Konstrukte auf höherer Ebene bereitgestellt.
Sperren sind Thread-Synchronisationsmechanismen, die im Wesentlichen dem gleichen Zweck wie synchronisierte Blöcke oder Schlüsselwörter dienen.
Intrinsic Locking
int count = 0; // shared among multiple threads
public void doSomething() {
synchronized(this) {
++count; // a non-atomic operation
}
}
Synchronisation mit Sperren
int count = 0; // shared among multiple threads
Lock lockObj = new ReentrantLock();
public void doSomething() {
try {
lockObj.lock();
++count; // a non-atomic operation
} finally {
lockObj.unlock(); // sure to release the lock without fail
}
}
Für Sperren stehen auch Funktionen zur Verfügung, die das intrinsische Sperren nicht bietet, z. B. Sperren, die jedoch auf Unterbrechungen ansprechen oder versuchen zu sperren.
Sperrung, reagiert auf Unterbrechung
class Locky {
int count = 0; // shared among multiple threads
Lock lockObj = new ReentrantLock();
public void doSomething() {
try {
try {
lockObj.lockInterruptibly();
++count; // a non-atomic operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // stopping
}
} finally {
if (!Thread.currentThread().isInterrupted()) {
lockObj.unlock(); // sure to release the lock without fail
}
}
}
}
Machen Sie nur etwas, wenn Sie sperren können
public class Locky2 {
int count = 0; // shared among multiple threads
Lock lockObj = new ReentrantLock();
public void doSomething() {
boolean locked = lockObj.tryLock(); // returns true upon successful lock
if (locked) {
try {
++count; // a non-atomic operation
} finally {
lockObj.unlock(); // sure to release the lock without fail
}
}
}
}
Es gibt mehrere Varianten von Schloss available.For mehr Details um die API - Dokumentation finden Sie hier