wait and notification () Metode în Java

1. Introducere

În acest articol, vom analiza unul dintre cele mai fundamentale mecanisme din Java - sincronizarea firelor.

Mai întâi vom discuta despre câțiva termeni și metodologii esențiale legate de concurență.

Și vom dezvolta o aplicație simplă - în care ne vom ocupa de probleme de concurență, cu scopul de a înțelege mai bine wait () și notification ().

2. Sincronizarea firului în Java

Într-un mediu cu mai multe fire, mai multe fire ar putea încerca să modifice aceeași resursă. Dacă firele nu sunt gestionate corect, acest lucru va duce, desigur, la probleme de consistență.

2.1. Blocuri păzite în Java

Un instrument pe care îl putem folosi pentru a coordona acțiunile mai multor fire în Java - sunt blocurile păzite. Astfel de blocuri păstrează o verificare pentru o anumită condiție înainte de a relua execuția.

Având în vedere acest lucru, vom folosi:

  • Object.wait () - pentru a suspenda un fir
  • Object.notify () - pentru a trezi un fir

Acest lucru poate fi mai bine înțeles din următoarea diagramă, care descrie ciclul de viață al unui fir :

Vă rugăm să rețineți că există multe modalități de a controla acest ciclu de viață; cu toate acestea, în acest articol, ne vom concentra doar pe wait () și notificare ().

3. wait () Metoda

Pur și simplu, când apelăm wait () - acest lucru forțează firul curent să aștepte până când un alt fir invocă notification () sau notificationAll () pe același obiect.

Pentru aceasta, firul curent trebuie să dețină monitorul obiectului. Potrivit lui Javadocs, acest lucru se poate întâmpla atunci când:

  • am executat metoda de instanță sincronizată pentru obiectul dat
  • am executat corpul unui bloc sincronizat pe obiectul dat
  • prin executarea de metode statice sincronizate pentru obiecte de tip Class

Rețineți că un singur fir activ poate deține monitorul unui obiect odată.

Această metodă wait () vine cu trei semnături supraîncărcate. Să aruncăm o privire la acestea.

3.1. aștepta()

Metoda wait () face ca firul curent să aștepte la nesfârșit până când un alt fir fie invocă notification () pentru acest obiect, fie notificationAll () .

3.2. așteptați (timp lung)

Folosind această metodă, putem specifica un timeout după care firul va fi trezit automat. Un fir poate fi trezit înainte de a atinge expirarea folosind notification () sau notificationAll ().

Rețineți că apelul de așteptare (0) este același cu apelul de așteptare ().

3.3. așteptați (expirare lungă, int nanos)

Aceasta este încă o semnătură care oferă aceeași funcționalitate, singura diferență fiind că putem oferi o precizie mai mare.

Perioada totală de expirare (în nanosecunde), este calculată ca 1_000_000 * expirare + nanos.

4. notifica () și notifica toate ()

Metoda notification () este utilizată pentru trezirea firelor care așteaptă un acces la monitorul acestui obiect.

Există două moduri de a notifica firele de așteptare.

4.1. notifica ()

Pentru toate firele care așteaptă pe monitorul acestui obiect (prin utilizarea oricărei metode wait () ), metoda notification () notifică pe oricare dintre ele să se trezească în mod arbitrar. Alegerea exactă a firului de a trezi nu este deterministă și depinde de implementare.

Deoarece notification () trezește un singur fir aleatoriu, acesta poate fi folosit pentru a implementa blocarea reciproc exclusivă în cazul în care firele efectuează sarcini similare, dar în majoritatea cazurilor, ar fi mai viabil să se implementeze notificationAll () .

4.2. notificationAll ()

Această metodă trezește pur și simplu toate firele care așteaptă pe monitorul acestui obiect.

Firele trezite se vor completa în modul obișnuit - ca orice alt fir.

Dar înainte de a permite executarea lor să continue, definiți întotdeauna o verificare rapidă a condiției necesare pentru a continua cu firul - deoarece pot exista anumite situații în care firul a fost trezit fără a primi o notificare (acest scenariu este discutat mai târziu într-un exemplu) .

5. Problemă de sincronizare expeditor-receptor

Acum, că înțelegem elementele de bază, să trecem printr-o aplicație simplă Sender - Receiver - care va folosi metodele wait () și notification () pentru a configura sincronizarea între ele:

  • Expeditorul ar trebui să trimită un pachet de date către receptor
  • Receptorul nu poate procesa pachetul de date până când Expeditor este terminat al trimite
  • În mod similar, Expeditorul nu trebuie să încerce să trimită alt pachet decât dacă Destinatarul a procesat deja pachetul anterior

Let's first create Data class that consists of the data packet that will be sent from Sender to Receiver. We'll use wait() and notifyAll() to set up synchronization between them:

public class Data { private String packet; // True if receiver should wait // False if sender should wait private boolean transfer = true; public synchronized void send(String packet) { while (!transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } transfer = false; this.packet = packet; notifyAll(); } public synchronized String receive() { while (transfer) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } transfer = true; notifyAll(); return packet; } }

Let's break down what's going on here:

  • The packet variable denotes the data that is being transferred over the network
  • We have a boolean variable transfer – which the Sender and Receiver will use for synchronization:
    • If this variable is true, then the Receiver should wait for Sender to send the message
    • If it's false, then Sender should wait for Receiver to receive the message
  • The Sender uses send() method to send data to the Receiver:
    • If transfer is false, we'll wait by calling wait() on this thread
    • But when it is true, we toggle the status, set our message and call notifyAll() to wake up other threads to specify that a significant event has occurred and they can check if they can continue execution
  • Similarly, the Receiver will use receive() method:
    • If the transfer was set to false by Sender, then only it will proceed, otherwise we'll call wait() on this thread
    • When the condition is met, we toggle the status, notify all waiting threads to wake up and return the data packet that was Receiver

5.1. Why Enclose wait() in a while Loop?

Since notify() and notifyAll() randomly wakes up threads that are waiting on this object's monitor, it's not always important that the condition is met. Sometimes it can happen that the thread is woken up, but the condition isn't actually satisfied yet.

We can also define a check to save us from spurious wakeups – where a thread can wake up from waiting without ever having received a notification.

5.2. Why Do We Need to Synchronize send() and receive() Methods?

We placed these methods inside synchronized methods to provide intrinsic locks. If a thread calling wait() method does not own the inherent lock, an error will be thrown.

We'll now create Sender and Receiver and implement the Runnable interface on both so that their instances can be executed by a thread.

Let's first see how Sender will work:

public class Sender implements Runnable { private Data data; // standard constructors public void run() { String packets[] = { "First packet", "Second packet", "Third packet", "Fourth packet", "End" }; for (String packet : packets) { data.send(packet); // Thread.sleep() to mimic heavy server-side processing try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } } }

For this Sender:

  • We're creating some random data packets that will be sent across the network in packets[] array
  • For each packet, we're merely calling send()
  • Then we're calling Thread.sleep() with random interval to mimic heavy server-side processing

Finally, let's implement our Receiver:

public class Receiver implements Runnable { private Data load; // standard constructors public void run() { for(String receivedMessage = load.receive(); !"End".equals(receivedMessage); receivedMessage = load.receive()) { System.out.println(receivedMessage); // ... try { Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); Log.error("Thread interrupted", e); } } } }

Here, we're simply calling load.receive() in the loop until we get the last “End” data packet.

Let's now see this application in action:

public static void main(String[] args) { Data data = new Data(); Thread sender = new Thread(new Sender(data)); Thread receiver = new Thread(new Receiver(data)); sender.start(); receiver.start(); }

We'll receive the following output:

First packet Second packet Third packet Fourth packet 

And here we are – we've received all data packets in the right, sequential order and successfully established the correct communication between our sender and receiver.

6. Conclusion

In this article, we discussed some core synchronization concepts in Java; more specifically, we focused on how we can use wait() and notify() to solve interesting synchronization problems. And finally, we went through a code sample where we applied these concepts in practice.

Before we wind down here, it's worth mentioning that all these low-level APIs, such as wait(), notify() and notifyAll() – are traditional methods that work well, but higher-level mechanism are often simpler and better – such as Java's native Lock and Condition interfaces (available in java.util.concurrent.locks package).

Pentru mai multe informații despre pachetul java.util.concurrent , accesați prezentarea noastră generală a articolului java.util.concurrent, iar Blocarea și condiția sunt prezentate în ghidul java.util.concurrent.Locks, aici.

Ca întotdeauna, fragmentele de cod complete utilizate în acest articol sunt disponibile pe GitHub.