vänta och meddela () Metoder i Java

1. Introduktion

I den här artikeln tittar vi på en av de mest grundläggande mekanismerna i Java - trådsynkronisering.

Vi kommer först att diskutera några väsentliga samtidiga termer och metoder.

Och vi kommer att utveckla en enkel applikation - där vi hanterar problem med samtidighet, med målet att bättre förstå vänta () och meddela ().

2. Trådsynkronisering i Java

I en flertrådad miljö kan flera trådar försöka ändra samma resurs. Om trådar inte hanteras ordentligt leder det naturligtvis till konsekvensproblem.

2.1. Bevakade block i Java

Ett verktyg vi kan använda för att samordna åtgärder av flera trådar i Java - är skyddade block. Sådana block kontrollerar ett visst tillstånd innan de återupptar körningen.

Med detta i åtanke kommer vi att använda oss av:

  • Object.wait () - för att avbryta en tråd
  • Object.notify () - för att väcka en tråd

Detta kan förstås bättre från följande diagram, som skildrar livscykel Ämne :

Observera att det finns många sätt att kontrollera denna livscykel; i den här artikeln kommer vi dock bara att fokusera på vänta () och meddela ().

3. Vänta () -metoden

Enkelt uttryckt, när vi kallar vänta () - detta tvingar den aktuella tråden att vänta tills någon annan tråd åberopar notify () eller notifyAll () på samma objekt.

För detta måste den aktuella tråden äga objektets bildskärm. Enligt Javadocs kan detta hända när:

  • Vi har kört synkroniserad instansmetod för det angivna objektet
  • Vi har kört kroppen av ett synkroniserat block på det angivna objektet
  • genom att köra synkroniserade statiska metoder för objekt av typen Klass

Observera att endast en aktiv tråd kan äga ett objekts bildskärm åt gången.

Denna wait () -metod kommer med tre överbelastade signaturer. Låt oss ta en titt på dessa.

3.1. vänta()

Den väntetid () metoden bringar den aktuella tråden att vänta på obestämd tid tills en annan tråd antingen anropar anmäla () för detta objekt eller notifyAll () .

3.2. vänta (lång timeout)

Med den här metoden kan vi ange en timeout varefter tråden väcks automatiskt. En tråd kan väckas innan den når timeout med notify () eller notifyAll ().

Observera att samtal väntar (0) är detsamma som samtal väntar ().

3.3. vänta (lång timeout, int nanos)

Detta är ännu en signatur som ger samma funktionalitet, med den enda skillnaden att vi kan ge högre precision.

Den totala tidsgränsen (i nanosekunder) beräknas som 1_000_000 * timeout + nanos.

4. notify () och notifyAll ()

Den meddelar () metod används för att vakna upp trådar som väntar på en tillgång till detta objekt bildskärm.

Det finns två sätt att meddela väntande trådar.

4.1. meddela ()

För alla trådar som väntar på det här objektets bildskärm (genom att använda någon av wait () -metoden) meddelar metoden notify () någon av dem att vakna godtyckligt. Valet av exakt vilken tråd som ska vakna är icke-deterministisk och beror på implementeringen.

Eftersom notify () väcker en enda slumpmässig tråd kan den användas för att implementera ömsesidigt uteslutande låsning där trådar gör liknande uppgifter, men i de flesta fall vore det mer lönsamt att implementera notifyAll () .

4.2. notifyAll ()

Denna metod väcker helt enkelt alla trådar som väntar på det här objektets bildskärm.

De väckta trådarna kommer att slutföras på vanligt sätt - som alla andra trådar.

Men innan vi tillåter att deras körning fortsätter, definiera alltid en snabb kontroll av det villkor som krävs för att fortsätta med tråden - eftersom det kan finnas vissa situationer där tråden vaknade utan att få ett meddelande (detta scenario diskuteras senare i ett exempel) .

5. Synkroniseringsproblem för avsändare / mottagare

Nu när vi förstår grunderna, låt oss gå igenom en enkel avsändare - mottagarapplikation - som använder wait () och meddelar () metoder för att ställa in synkronisering mellan dem:

  • Den Sender är tänkt att sända ett datapaket till mottagaren
  • Det Mottagaren kan inte behandla datapaketet tills Sender är klar skickar den
  • På samma sätt får avsändaren inte försöka skicka ett annat paket om inte mottagaren redan har behandlat det tidigare paketet

Låt oss först skapa Data klass som består av datapaket som skickas från avsändare till mottagare. Vi använder wait () och notifyAll () för att ställa in synkronisering mellan dem:

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; } }

Låt oss bryta ner vad som händer här:

  • Den paket variabel betecknar de data som överförs över nätverket
  • Vi har en boolesk variabelöverföring - som avsändaren och mottagaren kommer att använda för synkronisering:
    • Om denna variabel är sant , då mottagaren ska vänta på Sender för att skicka meddelandet
    • 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).

För mer information om paketet java.util.concurrent , besök vår översikt över artikeln java.util.concurrent och Lås och tillstånd beskrivs i guiden till java.util.concurrent.Locks, här.

Som alltid finns de fullständiga kodavsnitten som används i den här artikeln tillgängliga på GitHub.