1. 개요

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

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

그리고 wait()notify() 를 더 잘 이해하기 위해 동시성 문제를 처리하는 간단한 응용 프로그램을 개발할 것입니다 .

2. 자바에서의 쓰레드 동기화

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

2.1. Java의 보호된 블록

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

이를 염두에 두고 다음을 사용할 것입니다.

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

Java - 대기 및 알림

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

3. wait() 메소드

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

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

  • 주어진 객체에 대해 동기화된 인스턴스 메소드를 실행했을 때
  • 주어진 객체에서 동기화된 블록 의 본문을 실행할 때
  • 클래스 유형의 개체에 대해 동기화된 정적 메서드를 실행하여

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

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

3.1. 기다리다()

wait() 메서드 는 다른 스레드 가 이 객체에 대해 notify() 또는 notifyAll( ) 을 호출할 때까지 현재 스레드가 무기한 대기하도록 합니다 .

3.2. 대기(긴 시간 초과)

이 방법을 사용하여 스레드가 자동으로 깨어나는 시간 초과를 지정할 수 있습니다. notify( ) 또는 notifyAll() 을 사용하여 시간 초과에 도달하기 전에 스레드를 깨울 수 있습니다 .

wait(0) 호출 은 wait() 호출과 동일합니다 .

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

이것은 동일한 기능을 제공하는 또 다른 서명입니다. 여기서 유일한 차이점은 더 높은 정밀도를 제공할 수 있다는 것입니다.

총 시간 초과 기간(나노초)은 1_000_000*timeout + nanos 로 계산됩니다 .

4. notify()notifyAll()

이 개체의 모니터에 대한 액세스를 기다리고 있는 스레드를 깨우기 위해 notify() 메서드를 사용합니다 .

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

4.1. 통지()

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

notify() 는 단일 임의 스레드를 깨우기 때문에 스레드가 유사한 작업을 수행하는 상호 배타적 잠금을 구현하는 데 사용할 수 있습니다. 그러나 대부분의 경우 notifyAll() 을 구현하는 것이 더 실행 가능합니다 .

4.2. 모든 알림()

이 메서드는 단순히 이 개체의 모니터에서 대기 중인 모든 스레드를 깨웁니다.

깨어난 스레드는 이 개체에서 동기화를 시도하는 다른 스레드와 마찬가지로 일반적인 방식으로 경쟁합니다.

그러나 실행을 계속하기 전에 항상 스레드를 진행하는 데 필요한 조건에 대한 빠른 검사를 정의하십시오. 스레드가 알림을 받지 않고 깨어난 상황이 있을 수 있기 때문입니다(이 시나리오는 나중에 예제에서 설명함).

5. 발신자-수신자 동기화 문제

이제 기본 사항을 이해했으므로 wait()notify() 메서드를 사용하여 동기화를 설정 하는 간단한 SenderReceiver 응용 프로그램을 살펴 보겠습니다.

  • SenderReceiver 에게 데이터 패킷을 보내야 합니다 .
  • 수신자발신자 가 전송을 마칠 때까지 데이터 패킷을 처리할 수 없습니다 .
  • 마찬가지로 발신자 는 수신자 가 이전 패킷을 이미 처리 하지 않은 한 다른 패킷을 보내려고 시도해서는 안 됩니다.

먼저 Sender 에서 Receiver 로 보낼 데이터 패킷 으로 구성된 Data 클래스를 만들어 보겠습니다 . wait()notifyAll() 을 사용 하여 둘 사이의 동기화를 설정합니다.

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

여기서 무슨 일이 일어나고 있는지 분석해 보겠습니다.

  • 패킷 변수는 네트워크를 통해 전송되는 데이터를 나타냅니다 .
  • SenderReceiver동기화에 사용할 부울 변수 transfer 가 있습니다.
    • 이 변수가 true 이면 수신자 발신자 가 메시지를 보낼 때까지 기다려야 합니다.
    • false 이면 보낸 사람 은 받는 사람 이 메시지를 받을 까지 기다려야 합니다.
  • Sendersend() 메서드를 사용하여 Receiver에 데이터 보냅니다 .
    • transferfalse 이면 이 스레드에서 wait() 를 호출하여 기다립니다.
    • 그러나 true 이면 상태를 토글하고 메시지를 설정하고 notifyAll() 을 호출하여 다른 스레드를 깨워 중요한 이벤트가 발생했으며 실행을 계속할 수 있는지 확인할 수 있음을 지정합니다.
  • 마찬가지로 수신기receive() 메서드를 사용합니다.
    • Sender 에 의해 전송 이 false 로 설정된 경우 에만 진행되며, 그렇지 않으면 이 스레드에서 wait() 를 호출합니다.
    • 조건이 충족되면 상태를 토글하고 대기 중인 모든 스레드에 깨어나도록 알리고 수신된 데이터 패킷을 반환합니다.

5.1. 왜 wait()while 루프 로 묶 나요?

notify ()notifyAll() 은 이 개체의 모니터를 기다리고 있는 스레드를 무작위로 깨우기 때문에 조건이 충족되는 것이 항상 중요한 것은 아닙니다 . 때때로 스레드가 깨어나지만 조건이 아직 실제로 만족되지 않습니다.

스레드가 알림을 받지 않고도 대기 상태에서 깨어날 수 있는 가짜 깨우기에서 우리를 구하기 위해 검사를 정의할 수도 있습니다.

5.2. end()receive() 메서드 를 동기화해야 하는 이유는 무엇 입니까?

내장 잠금을 제공하기 위해 이러한 메서드를 동기화된 메서드 안에 배치 했습니다. wait() 메서드 를 호출하는 스레드 가 고유 잠금을 소유하지 않으면 오류가 발생합니다.

이제 SenderReceiver 를 만들고 둘 다에 Runnable 인터페이스를 구현하여 스레드에서 인스턴스를 실행할 수 있도록 합니다.

먼저 Sender 가 어떻게 작동 하는지 살펴보겠습니다 .

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(); 
                System.err.println("Thread Interrupted"); 
            }
        }
    }
}

이 Sender 를 자세히 살펴보겠습니다 .

  • 패킷[] 배열 에서 네트워크를 통해 전송될 랜덤의 데이터 패킷을 생성합니다 .
  • 각 패킷에 대해 단순히 send()를 호출합니다.
  • 그런 다음 무거운 서버 측 처리를 모방하기 위해 랜덤의 간격으로 Thread.sleep() 을 호출 합니다.

마지막으로 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);

            //Thread.sleep() to mimic heavy server-side processing
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(1000, 5000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); 
                System.err.println("Thread Interrupted"); 
            }
        }
    }
}

여기서는 마지막 "End" 데이터 패킷 을 얻을 때까지 루프에서 load.receive() 를 호출하기만 하면 됩니다.

이제 이 애플리케이션이 작동하는 모습을 살펴보겠습니다.

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

다음 출력을 받게 됩니다.

First packet
Second packet
Third packet
Fourth packet

그리고 여기 있습니다. 모든 데이터 패킷을 올바른 순서대로 수신했으며 발신자와 수신자 간의 올바른 통신을 성공적으로 설정했습니다.

6. 결론

이 기사에서는 Java의 몇 가지 핵심 동기화 개념에 대해 논의했습니다. 보다 구체적으로, 흥미로운 동기화 문제를 해결하기 위해 wait()notify() 를 사용하는 방법에 중점을 두었습니다. 마지막으로 이러한 개념을 실제로 적용한 코드 샘플을 살펴보았습니다.

닫기 전에 wait() , notify()notifyAll( ) 과 같은 이러한 모든 저수준 API 는 잘 작동하는 전통적인 방법이지만 Java와 같은 고수준 메커니즘은 종종 더 간단하고 더 좋습니다. 기본 잠금조건 인터페이스( java.util.concurrent.locks 패키지에서 사용 가능).

java.util.concurrent 패키지 에 대한 자세한 내용 은 java.util.concurrent 문서 개요를 참조하세요. 잠금조건 은 java.util.concurrent.Locks 사용방법(예제) 에서 다룹니다 .

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

Generic footer banner