wait and notify () Metody w Javie

1. Wstęp

W tym artykule przyjrzymy się jednemu z najbardziej podstawowych mechanizmów w Javie - synchronizacji wątków.

Najpierw omówimy kilka podstawowych terminów i metodologii związanych ze współbieżnością.

Opracujemy też prostą aplikację - w której zajmiemy się problemami współbieżności, mając na celu lepsze zrozumienie funkcji wait () i notify ().

2. Synchronizacja wątków w Javie

W środowisku wielowątkowym wiele wątków może próbować modyfikować ten sam zasób. Jeśli wątki nie są odpowiednio zarządzane, prowadzi to oczywiście do problemów ze spójnością.

2.1. Chronione bloki w Javie

Jednym z narzędzi, których możemy użyć do koordynowania działań wielu wątków w Javie - są chronione bloki. Takie bloki sprawdzają określony stan przed wznowieniem wykonywania.

Mając to na uwadze, wykorzystamy:

  • Object.wait () - aby zawiesić wątek
  • Object.notify () - aby obudzić wątek

Można to lepiej zrozumieć na poniższym diagramie, który przedstawia cykl życia wątku :

Należy pamiętać, że istnieje wiele sposobów kontrolowania tego cyklu życia; jednak w tym artykule skupimy się tylko na wait () i notify ().

3. wait () Metoda

Mówiąc najprościej, gdy wywołujemy wait () - wymusza to na bieżącym wątku czekanie, aż inny wątek wywoła notify () lub notifyAll () na tym samym obiekcie.

W tym celu bieżący wątek musi być właścicielem monitora obiektu. Według Javadocs może się to zdarzyć, gdy:

  • wykonaliśmy zsynchronizowaną metodę instancji dla danego obiektu
  • wykonaliśmy ciało zsynchronizowanego bloku na danym obiekcie
  • wykonując zsynchronizowane metody statyczne dla obiektów typu Class

Zauważ, że tylko jeden aktywny wątek może być właścicielem monitora obiektu naraz.

Ta metoda wait () zawiera trzy przeciążone sygnatury. Rzućmy okiem na to.

3.1. czekać()

Metoda wait () powoduje, że bieżący wątek czeka przez czas nieokreślony, aż inny wątek wywoła funkcję notify () dla tego obiektu lub notifyAll () .

3.2. czekaj (długi czas oczekiwania)

Korzystając z tej metody, możemy określić limit czasu, po którym wątek zostanie automatycznie obudzony. Wątek można obudzić przed osiągnięciem limitu czasu za pomocą notify () lub notifyAll ().

Zauważ, że wywołanie wait (0) jest tym samym, co wywołanie wait ().

3.3. czekaj (długi czas oczekiwania, int nanos)

To kolejna sygnatura zapewniająca tę samą funkcjonalność, z tą różnicą, że możemy zapewnić większą precyzję.

Całkowity limit czasu (w nanosekundach) jest obliczany jako 1_000_000 * limit czasu + nanos.

4. notify () and notifyAll ()

Metoda notify () służy do budzenia wątków oczekujących na dostęp do monitora tego obiektu.

Istnieją dwa sposoby powiadamiania o oczekujących wątkach.

4.1. notyfikować()

Dla wszystkich wątków oczekujących na monitorze tego obiektu (przy użyciu dowolnej metody wait () ), metoda notify () powiadamia dowolny z nich o arbitralnym wybudzeniu. Wybór, który dokładnie wątek ma zostać wybudzony, nie jest deterministyczny i zależy od implementacji.

Ponieważ notify () budzi pojedynczy losowy wątek, można go użyć do zaimplementowania wzajemnie wykluczającego się blokowania, w którym wątki wykonują podobne zadania, ale w większości przypadków bardziej opłacalne byłoby zaimplementowanie notifyAll () .

4.2. notifyAll ()

Ta metoda po prostu budzi wszystkie wątki, które oczekują na monitorze tego obiektu.

Przebudzone wątki zakończą się w zwykły sposób - jak każdy inny wątek.

Zanim jednak pozwolimy na kontynuację ich wykonywania, zawsze definiuj szybkie sprawdzenie warunku wymaganego do kontynuowania wątku - ponieważ mogą wystąpić sytuacje, w których wątek został obudzony bez otrzymania powiadomienia (ten scenariusz zostanie omówiony w dalszej części przykładu) .

5. Problem z synchronizacją nadawcy i odbiorcy

Teraz, gdy rozumiemy już podstawy, przejdźmy do prostej aplikacji Sender - Receiver - która będzie korzystać z metod wait () i notify () () , aby ustawić synchronizację między nimi:

  • Sender ma wysłać pakiet danych do odbiornika
  • Odbiornik nie może przetworzyć pakiet danych aż Sender zakończeniu wysłaniem
  • Podobnie, nadawca nie może próbować wysłać kolejnego pakietu, chyba że odbiorca przetworzył już poprzedni pakiet

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).

Aby uzyskać więcej informacji na temat java.util.concurrent pakietu, odwiedź naszą przegląd artykułu java.util.concurrent i blokady i Stan zostały omówione w przewodniku java.util.concurrent.Locks, tutaj.

Jak zawsze, pełne fragmenty kodu użyte w tym artykule są dostępne na GitHub.