1. 개요
이 예제에서는 Java에서 가장 기본적인 메커니즘 중 하나인 스레드 동기화를 살펴보겠습니다.
먼저 몇 가지 필수적인 동시성 관련 용어와 방법론에 대해 논의합니다.
그리고 wait() 및 notify() 를 더 잘 이해하기 위해 동시성 문제를 처리하는 간단한 응용 프로그램을 개발할 것입니다 .
2. 자바에서의 쓰레드 동기화
다중 스레드 환경에서 여러 스레드가 동일한 리소스를 수정하려고 할 수 있습니다. 스레드를 제대로 관리하지 않으면 물론 일관성 문제가 발생합니다.
2.1. Java의 보호된 블록
Java에서 여러 스레드의 작업을 조정하는 데 사용할 수 있는 도구 중 하나는 보호 블록입니다. 이러한 블록은 실행을 재개하기 전에 특정 조건을 계속 확인합니다.
이를 염두에 두고 다음을 사용할 것입니다.
- 스레드를 일시 중단하는 Object.wait()
- 스레드를 깨우기 위한 Object.notify()
스레드 의 수명 주기를 나타내는 다음 다이어그램에서 이를 더 잘 이해할 수 있습니다 .
이 수명 주기를 제어하는 방법에는 여러 가지가 있습니다. 그러나 이 기사에서는 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() 메서드를 사용하여 동기화를 설정 하는 간단한 Sender – Receiver 응용 프로그램을 살펴 보겠습니다.
- Sender 는 Receiver 에게 데이터 패킷을 보내야 합니다 .
- 수신자 는 발신자 가 전송을 마칠 때까지 데이터 패킷을 처리할 수 없습니다 .
- 마찬가지로 발신자 는 수신자 가 이전 패킷을 이미 처리 하지 않은 한 다른 패킷을 보내려고 시도해서는 안 됩니다.
먼저 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();
}
}
여기서 무슨 일이 일어나고 있는지 분석해 보겠습니다.
- 패킷 변수는 네트워크를 통해 전송되는 데이터를 나타냅니다 .
- Sender 와 Receiver 가 동기화에 사용할 부울 변수 transfer 가 있습니다.
- 이 변수가 true 이면 수신자 는 발신자 가 메시지를 보낼 때까지 기다려야 합니다.
- false 이면 보낸 사람 은 받는 사람 이 메시지를 받을 때 까지 기다려야 합니다.
- Sender 는 send() 메서드를 사용하여 Receiver에 데이터 를 보냅니다 .
- transfer 가 false 이면 이 스레드에서 wait() 를 호출하여 기다립니다.
- 그러나 true 이면 상태를 토글하고 메시지를 설정하고 notifyAll() 을 호출하여 다른 스레드를 깨워 중요한 이벤트가 발생했으며 실행을 계속할 수 있는지 확인할 수 있음을 지정합니다.
- 마찬가지로 수신기 는 receive() 메서드를 사용합니다.
- Sender 에 의해 전송 이 false 로 설정된 경우 에만 진행되며, 그렇지 않으면 이 스레드에서 wait() 를 호출합니다.
- 조건이 충족되면 상태를 토글하고 대기 중인 모든 스레드에 깨어나도록 알리고 수신된 데이터 패킷을 반환합니다.
5.1. 왜 wait() 를 while 루프 로 묶 나요?
notify () 및 notifyAll() 은 이 개체의 모니터를 기다리고 있는 스레드를 무작위로 깨우기 때문에 조건이 충족되는 것이 항상 중요한 것은 아닙니다 . 때때로 스레드가 깨어나지만 조건이 아직 실제로 만족되지 않습니다.
스레드가 알림을 받지 않고도 대기 상태에서 깨어날 수 있는 가짜 깨우기에서 우리를 구하기 위해 검사를 정의할 수도 있습니다.
5.2. end() 및 receive() 메서드 를 동기화해야 하는 이유는 무엇 입니까?
내장 잠금을 제공하기 위해 이러한 메서드를 동기화된 메서드 안에 배치 했습니다. wait() 메서드 를 호출하는 스레드 가 고유 잠금을 소유하지 않으면 오류가 발생합니다.
이제 Sender 와 Receiver 를 만들고 둘 다에 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에서 사용할 수 있습니다.