Ich schlage vor, mehrere Möglichkeiten zur Synchronisierung von Threads und Prozessen in Betracht zu ziehen, die am häufigsten in Java verwendeten. Sie unterscheiden sich in der Implementierung und den Anwendungsfällen. Wir werden alle Methoden mit interessanten Beispielen betrachten.
Kritischer Abschnitt
Diese Methode ist für Sie geeignet, wenn:
- parallele Threads arbeiten mit einer gemeinsam genutzten Ressource;
- erfordert Synchronisation zwischen Threads, nicht zwischen Prozessen;
Diese Synchronisierungsmethode wird als Ressourcensynchronisierung ("Öffnen-Schließen-Synchronisierung") bezeichnet. Die Idee hinter dieser Methode ist, dass jedem Objekt in Java ein Monitor zugeordnet ist. Ein Monitor ist eine Art Werkzeug, um den Zugriff auf ein Objekt zu kontrollieren.
Die synchronisierte Anweisung wird verwendet, um einen kritischen Abschnitt zu erstellen. Wenn die Codeausführung die synchronisierte Anweisung erreicht, wird der Objektmonitor blockiert. Zum Zeitpunkt einer Sperre hat nur ein Thread, der die Sperre erworben hat, exklusiven Zugriff auf einen Codeblock. Nachdem der Codeblock abgeschlossen ist, wird der Objektmonitor freigegeben und anderen Threads zur Verfügung gestellt.
Wenn der Monitor freigegeben wird, wird er von einem anderen Thread erfasst, und alle anderen Threads warten weiterhin darauf, dass er freigegeben wird.
Beispiel
Stellen Sie sich vor, wie ein Online-Einzelhandelsgeschäft funktioniert. Nachdem einer der Käufer das Produkt in den Warenkorb gelegt hat, soll das verbleibende Produkt gezählt werden. Erst danach kann der andere Käufer die gewünschte Menge an Waren in den Warenkorb legen. Danach kaufen Sie sie.
public class Program { private static final Boolean key = true; //the object will be used for synchronization //it must be final private static Integer amountOfGoods = 20; public static class Buyer implements Runnable { @Override public void run() { final int goodsInTheShoppingCart = (int) (1 + Math.random() * amountOfGoods); //the number of goods that the buyer will put in the basket synchronized (key) { if (amountOfGoods - goodsInTheShoppingCart >= 0) { System.out.println("The " + Thread.currentThread().getName() + " placed " + goodsInTheShoppingCart + " items in the basket."); if ((amountOfGoods = amountOfGoods - goodsInTheShoppingCart) < 0) { System.out.println("Whoops! The product is out of stock, but the " + Thread.currentThread().getName() + " didn't know about it."); return; } else { System.out.println("There are " + amountOfGoods + " items left in the store."); } } else { System.out.println("The " + Thread.currentThread().getName() + " is notified that the goods are over."); return; } } System.out.println("The " + Thread.currentThread().getName() + " made a purchase."); } } public static void main(String[] args) { for (int i = 1; i <= 5; i++) { Thread t = new Thread(new Buyer()); // create five threads t.setName("buyer " + i); System.out.println("The " + t.getName() + " went to the store website."); t.start(); } } }
Programmablauf vor Verwendung des Synchronisationsbausteins
Wir sehen, dass Threads Programmcode auf zufällige Weise ausführen. Aus diesem Grund legen "Käufer" die Waren in die Körbe, bleiben aber ohne Waren.
Lassen Sie uns nun einen Synchronisationsblock hinzufügen und sehen, wie sich das Programm ändert.
Jetzt funktioniert das Programm genau so, wie wir es erwartet haben. Groß!
Schnittstellenkanal
Sie können eine Schnittstellen-Pipe verwenden, wenn Sie verschiedene Prozesse synchronisieren müssen. Der Kanal wird auf JVM-Ebene erstellt und ist im System identisch.
>Eine Pipe ist eine offene Verbindung zu einem Objekt, z. B. einem Hardwaregerät, einer Datei, einem Netzwerk-Socket oder einer Softwarekomponente, die in der Lage ist, eine oder mehrere verschiedene E/A-Operationen auszuführen, z. B. Lesen oder Schreiben.
> Der Kanal ist entweder offen oder geschlossen. Der Kanal ist geöffnet, wenn er erstellt wird, und bleibt geschlossen, nachdem er geschlossen wurde. Sobald ein Kanal geschlossen ist, löst jeder Versuch, eine E/A-Operation darauf aufzurufen, eine
ClosedChannelException
aus. Ob ein Kanal geöffnet ist, kann durch Aufrufen seiner Methoden isOpen() und tryLock() überprüft werden. ).
Java package.nio.channels enthält Kanäle wie:
- Asynchroner Kanal
- ByteKanal
- Netzwerkkanal
- Dateikanal
- Unterbrechbarer Kanal
- MulticastChannel und andere.
Beispiel
Zwei Prozesse schreiben Daten in eine Datei. Sie sollten sich bei der Aufnahme abwechseln, ohne sich gegenseitig bei der Arbeit zu stören.
Lassen Sie uns zwei Projekte erstellen, die sich nur darin unterscheiden, dass sie in eine Datei geschrieben werden - "Programm 1" oder "Programm 2". Wir betreiben sie gleichzeitig.
public class Program { public static void main(String[] args) throws InterruptedException, IOException { File file = new File("C:\\text.txt"); FileChannel fileChannel = new FileOutputStream(file, true).getChannel(); while(fileChannel.tryLock() == null){ // the process checks if the channel is free to write to the file Thread.sleep(10000); } try { String lineSeparator = System.getProperty("line.separator"); for (int i = 0; i < 10; i++) { String newData = "The program 2 is recording." + lineSeparator; ByteBuffer buf = ByteBuffer.allocate(200); buf.put(newData.getBytes()); buf.flip(); fileChannel.write(buf); Thread.sleep(1000); } } catch (IOException e) { e.printStackTrace(); } fileChannel.close(); } }
Wenn wir die klassische FileWriter-Methode zum Schreiben in eine Datei verwenden würden, würden wir etwas Ähnliches in der Datei finden.
Sehen wir uns nun an, was in der Datei enthalten sein wird, wenn wir Kanäle verwenden.
Genau das brauchen wir! Prozesse greifen nacheinander auf die Datei zu.
Entwicklungen
Zusätzlich zur Datensynchronisation gibt es eine Synchronisation nach Ereignissen. Wenn gleichzeitig laufende Threads angehalten werden, bis ein Ereignis von einem anderen Thread ausgelöst wird. Die Hauptoperationen für diese Art der Synchronisierung sind Warten und Benachrichtigen.
Dies ist die "wait-notify"-Synchronisierung:
- parallele Threads warten auf ein Ereignis;
- Einer der Threads ist für das Auslösen des Ereignisses verantwortlich und benachrichtigt die anderen Threads (einen mit "notify" oder alle mit "notifyAll"), indem er sie entsperrt:
- nur zum Synchronisieren von Threads geeignet, nicht von Prozessen;
Beispiel
Stellen wir uns vor, wie das automatische Waffennachladesystem funktioniert. Zum Beispiel sind nur 15 Runden auf Lager. Das Magazin fasst nur 6. Der Schütze will die ganze Munition verbrauchen.
public class Program { public static void main(String[] args) { WeaponStore weaponStore = new WeaponStore(); Arsenal arsenal = new Arsenal(weaponStore); Shooter shooter = new Shooter(weaponStore); new Thread(arsenal).start(); new Thread(shooter).start(); } } class WeaponStore { private int cartridges = 0; public synchronized void shot() { // synchronization of the method is required // because two threads will have access to it while (cartridges < 1) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //was thrown out notify by another thread, then the cartridge is received cartridges--; System.out.println("Produced 1 shot." + " Cartridges in the store: " + cartridges); notify(); } public synchronized void reload(int arsenalCartriges) { // synchronization of the method is required // because two threads will have access to it while (cartridges >= 6) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } cartridges++; System.out.println("1 cartridge has been added to the store." + " Cartridges in stock: " + arsenalCartriges); notify(); } } class Arsenal implements Runnable{ private int cartridges = 15; private WeaponStore weapon; Arsenal(WeaponStore weapon){ this.weapon = weapon; } public void run(){ for (int cartridges = 15; cartridges > 0; cartridges--) { weapon.reload(cartridges); } } } class Shooter implements Runnable{ private WeaponStore weapon; Shooter(WeaponStore weapon){ this.weapon = weapon; } public void run(){ for (int i = 1; i <= 15; i++) { weapon.shot(); } } }
Programmergebnis
1 cartridge has been added to the store. Cartridges in stock: 15 1 cartridge has been added to the store. Cartridges in stock: 14 1 cartridge has been added to the store. Cartridges in stock: 13 1 cartridge has been added to the store. Cartridges in stock: 12 1 cartridge has been added to the store. Cartridges in stock: 11 Produced 1 shot. Cartridges in the store: 4 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0 1 cartridge has been added to the store. Cartridges in stock: 10 1 cartridge has been added to the store. Cartridges in stock: 9 1 cartridge has been added to the store. Cartridges in stock: 8 1 cartridge has been added to the store. Cartridges in stock: 7 1 cartridge has been added to the store. Cartridges in stock: 6 1 cartridge has been added to the store. Cartridges in stock: 5 Produced 1 shot. Cartridges in the store: 5 Produced 1 shot. Cartridges in the store: 4 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0 1 cartridge has been added to the store. Cartridges in stock: 4 1 cartridge has been added to the store. Cartridges in stock: 3 1 cartridge has been added to the store. Cartridges in stock: 2 1 cartridge has been added to the store. Cartridges in stock: 1 Produced 1 shot. Cartridges in the store: 3 Produced 1 shot. Cartridges in the store: 2 Produced 1 shot. Cartridges in the store: 1 Produced 1 shot. Cartridges in the store: 0
Fazit
Wie Sie sehen können, haben die oben besprochenen Synchronisierungsmethoden unterschiedliche Verwendungszwecke. Wählen Sie das Richtige und synchronisieren Sie Ihre Threads und Prozesse immer so, dass sie friedlich leben und sich nie gegenseitig bei der Arbeit stören.
Abonnieren Sie den Java-Bereich, wenn Sie weitere interessante Artikel wünschen. Schreiben Sie Ihre Meinung und Ideen zu diesem Thema in die Kommentare.