자바의 wait 및 notify () 메소드

1. 소개

이 기사에서는 Java의 가장 기본적인 메커니즘 중 하나 인 스레드 동기화를 살펴 보겠습니다.

먼저 몇 가지 필수적인 동시성 관련 용어 및 방법론에 대해 설명합니다.

그리고 우리는 wait ()notify () 를 더 잘 이해하기 위해 동시성 문제를 다룰 간단한 애플리케이션을 개발할 것입니다 .

2. 자바의 스레드 동기화

다중 스레드 환경에서는 여러 스레드가 동일한 리소스를 수정하려고 할 수 있습니다. 스레드가 제대로 관리되지 않으면 물론 일관성 문제가 발생합니다.

2.1. 자바의 보호 된 블록

Java에서 여러 스레드의 작업을 조정하는 데 사용할 수있는 도구 중 하나는 보호 된 블록입니다. 이러한 블록은 실행을 재개하기 전에 특정 조건을 확인합니다.

이를 염두에두고 다음을 사용합니다.

  • Object.wait () – 스레드 일시 중단
  • Object.notify () – 스레드 깨우기

Thread 의 수명주기를 나타내는 다음 다이어그램에서 더 잘 이해할 수 있습니다 .

이 수명주기를 제어하는 ​​방법에는 여러 가지가 있습니다. 그러나이 기사에서는 wait ()notify () 에만 초점을 맞출 것 입니다.

3. wait () 메서드

간단히 말해 wait ()를 호출하면 다른 스레드가 동일한 객체에서 notify () 또는 notifyAll () 을 호출 할 때까지 현재 스레드가 대기 하도록 합니다.

이를 위해 현재 스레드는 개체의 모니터를 소유해야합니다. Javadocs에 따르면 다음과 같은 경우에 발생할 수 있습니다.

  • 주어진 객체에 대해 동기화 된 인스턴스 메소드를 실행했습니다.
  • 주어진 객체에 대해 동기화 된 블록 의 본문을 실행했습니다.
  • Class 유형의 객체에 대해 동기화 된 정적 메서드를 실행하여

한 번에 하나의 활성 스레드 만 개체의 모니터를 소유 할 수 있습니다.

wait () 메서드는 세 개의 오버로드 된 서명과 함께 제공됩니다. 이것들을 살펴 보겠습니다.

3.1. 기다림()

대기 () 메소드는 현재 스레드가 다른 스레드 중 원용 때까지 무한정 대기시킨다 통지 () 이 오브젝트 또는 용 의 notifyAll () .

3.2. 대기 (긴 시간 초과)

이 방법을 사용하면 스레드가 자동으로 깨어날 시간 제한을 지정할 수 있습니다. 스레드는 사용 시간 제한에 도달하기 전에 깨어날 수있는 통지 () 또는 가는 notifyAll ().

호출합니다 대기 (0 것은) 호출과 동일 대기를 ().

3.3. wait (긴 시간 초과, int nanos)

이것은 동일한 기능을 제공하는 또 다른 시그니처이며, 유일한 차이점은 더 높은 정밀도를 제공 할 수 있다는 것입니다.

총 타임 아웃 기간 (나노초)은 1_000_000 * timeout + nanos 로 계산됩니다 .

4. notify ()notifyAll ()

통지 () 메소드는이 객체의 모니터에 대한 액세스를 기다리는 스레드를 깨워에 사용됩니다.

대기중인 스레드를 알리는 방법에는 두 가지가 있습니다.

4.1. 알림 ()

이 객체의 모니터에서 대기중인 모든 스레드 ( wait () 메서드 중 하나를 사용하여 )에 대해 notify () 메서드는 임의의 하나에게 깨어나도록 알립니다. 깨울 스레드를 정확히 선택하는 것은 비 결정적이며 구현에 따라 다릅니다.

Since notify() wakes up a single random thread it can be used to implement mutually exclusive locking where threads are doing similar tasks, but in most cases, it would be more viable to implement notifyAll().

4.2. notifyAll()

This method simply wakes all threads that are waiting on this object's monitor.

The awakened threads will complete in the usual manner – like any other thread.

But before we allow their execution to continue, always define a quick check for the condition required to proceed with the thread – because there may be some situations where the thread got woken up without receiving a notification (this scenario is discussed later in an example).

5. Sender-Receiver Synchronization Problem

Now that we understand the basics, let's go through a simple SenderReceiver application – that will make use of the wait() and notify() methods to set up synchronization between them:

  • The Sender is supposed to send a data packet to the Receiver
  • The Receiver cannot process the data packet until the Sender is finished sending it
  • Similarly, the Sender mustn't attempt to send another packet unless the Receiver has already processed the previous packet

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

java.util.concurrent 패키지 에 대한 자세한 내용 은 java.util.concurrent 기사의 개요를 참조하십시오. 잠금조건 은 java.util.concurrent.Locks 가이드에서 다룹니다.

항상 그렇듯이이 문서에 사용 된 전체 코드 조각은 GitHub에서 사용할 수 있습니다.