스레드 통신의 목표는 스레드가 서로 신호를 보낼 수 있도록 하는 것입니다. 반면에 스레드 통신을 사용하면 스레드가 다른 스레드의 신호를 기다릴 수 있습니다.
공유 객체를 통해 통신
Busy wait
wait(), inform() 및 informAll()
Lost signal
False wakeup
동일한 신호를 기다리는 여러 스레드
상수 문자열이나 전역 개체에 대해 wait()를 호출하지 마세요
공유 개체를 통해 통신
스레드 간에 신호를 보내는 간단한 방법은 공유 객체의 변수에 신호 값을 설정하는 것입니다. 스레드 A는 동기화된 블록에서 부울 멤버 변수 hasDataToProcess를 true로 설정하고 스레드 B도 동기화된 블록에서 멤버 변수 hasDataToProcess를 읽습니다. 이 간단한 예는 신호를 보유하고 설정 및 확인 메소드를 제공하는 객체를 사용합니다.
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
스레드 A와 B는 통신을 위해 MySignal의 공유 인스턴스에 대한 참조를 얻어야 합니다. 서로 다른 MySingal 인스턴스에 대한 참조를 보유하고 있으면 서로가 상대방의 신호를 감지할 수 없습니다. 처리해야 하는 데이터는 MySignal 인스턴스와 별도로 저장되는 공유 캐시에 저장할 수 있습니다.
Busy Wait
데이터 처리를 준비 중인 스레드 B는 데이터가 사용 가능해지기를 기다리고 있습니다. 즉, hasDataToProcess()가 true를 반환하도록 하는 스레드 A의 신호를 기다리고 있습니다. 스레드 B는 다음 신호를 기다리면서 루프에서 실행됩니다.
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
wait(), inform() 및 informAll()
Busy wait는 대기를 실행하는 CPU에 적용되지 않습니다. 평균 대기 시간이 매우 짧지 않은 한 스레드 활용률입니다. 그렇지 않으면 대기 중인 스레드가 대기 중인 신호를 수신할 때까지 대기 중인 스레드를 휴면 상태로 두거나 실행되지 않는 상태로 두는 것이 더 합리적입니다.
Java에는 신호를 기다리는 동안 스레드가 실행되지 않도록 하는 대기 메커니즘이 내장되어 있습니다. java.lang.Object 클래스는 이 대기 메커니즘을 구현하기 위해 wait(), inform() 및 informAll()의 세 가지 메소드를 정의합니다.
스레드가 개체의 wait() 메서드를 호출하면 다른 스레드가 동일한 개체의 inform() 메서드를 호출할 때까지 실행되지 않는 상태가 됩니다. wait() 또는 inform()을 호출하려면 스레드가 먼저 해당 객체에 대한 잠금을 획득해야 합니다. 즉, 스레드는 동기화된 블록에서 wait() 또는 inform()을 호출해야 합니다. 다음은 wait() 및 inform()을 사용하여 MySingal - MyWaitNotify의 수정된 버전입니다.
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
대기 스레드는 doWait()를 호출하고, 깨어 있는 스레드는 doNotify()를 호출합니다. 스레드가 객체의 inform() 메서드를 호출하면 해당 객체를 기다리고 있는 모든 스레드 중 하나의 스레드가 깨어나 실행이 허용됩니다. (참고: 깨어날 스레드는 무작위이며 어떤 스레드를 깨울지 지정할 수 없습니다.) . 또한 주어진 객체를 기다리고 있는 모든 스레드를 깨우기 위한 informAll() 메서드를 제공합니다.
보시다시피 대기 스레드와 깨우기 스레드 모두 동기화된 블록에서 wait() 및 inform()을 호출합니다. 이것은 필수입니다! 스레드가 객체 잠금을 보유하지 않으면 스레드는 wait(), inform() 또는 informAll()을 호출할 수 없습니다. 그렇지 않으면 IllegalMonitorStateException 예외가 발생합니다.
(참고: JVM이 구현되는 방식입니다. wait를 호출하면 먼저 현재 스레드가 잠금의 소유자인지 확인합니다. 그렇지 않으면 IllegalMonitorStateExcept가 발생합니다.)
그런데 이게 어떻게 가능하지? 동기화된 블록에서 대기 중인 스레드가 실행되면 항상 모니터 개체(myMonitor 개체)의 잠금을 유지하지 않나요? 대기 스레드가 깨어 있는 스레드가 doNotify()의 동기화된 블록에 진입하는 것을 차단할 수 없나요? 대답은: 실제로는 그렇지 않습니다. 스레드가 wait() 메서드를 호출하면 모니터 개체에 대해 보유하고 있는 잠금이 해제됩니다. 이렇게 하면 다른 스레드도 wait() 또는 inform()을 호출할 수 있습니다.
스레드가 활성화되면 통지()를 호출하는
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } } <br>
스레드가 자체 동기화 블록을 종료할 때까지 wait() 메소드 호출을 즉시 종료할 수 없습니다. 즉, 대기 메소드 호출은 동기화된 블록에서 실행되기 때문에 깨어난 스레드는 wait() 메소드 호출을 종료하기 전에 모니터 객체의 잠금을 다시 획득해야 합니다. 여러 스레드가 informAll()에 의해 깨어나는 경우 각 스레드는 wait()를 종료하기 전에 모니터 객체의 잠금을 획득해야 하므로 단 하나의 스레드만 동시에 wait() 메서드를 종료할 수 있습니다.
누락된 신호
inform() 및 informAll() 메소드는 호출한 메소드를 저장하지 않습니다. 왜냐하면 이 두 메소드가 호출될 때 대기 상태에 있는 스레드가 없을 가능성이 있기 때문입니다. . 알림 신호는 만료된 후 삭제됩니다. 따라서 통지된 스레드가 wait()를 호출하기 전에 스레드가 inform()을 호출하면 대기 중인 스레드는 신호를 놓치게 됩니다. 이는 문제가 될 수도 있고 아닐 수도 있습니다. 그러나 어떤 경우에는 스레드가 깨우기 신호를 놓쳤기 때문에 대기 중인 스레드가 영원히 기다리다가 결코 깨어나지 않을 수도 있습니다.
신호 손실을 방지하려면 신호 클래스에 저장해야 합니다. MyWaitNotify 예제에서 알림 신호는 MyWaitNotify 인스턴스의 멤버 변수에 저장되어야 합니다. MyWaitNotify의 수정된 버전은 다음과 같습니다.
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 doNotify()方法在调用 notify()前把 wasSignalled 变量设为 true。同时,留意 doWait()方法在调用 wait()前会检查 wasSignalled 变量。事实上,如果没有信号在前一次 doWait()调用和这次 doWait()调用之间的时间段里被接收到,它将只调用 wait()。
(校注:为了避免信号丢失, 用一个变量来保存是否被通知过。在 notify 前,设置自己已经被通知过。在 wait 后,设置自己没有被通知过,需要等待通知。)
假唤醒
由于莫名其妙的原因,线程有可能在没有调用过 notify()和 notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。
如果在 MyWaitNotify2 的 doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。
为了防止假唤醒,保存信号的成员变量将在一个 while 循环里接受检查,而不是在 if 表达式里。这样的一个 while 循环叫做自旋锁(校注:这种做法要慎重,目前的 JVM 实现自旋会消耗 CPU,如果长时间不调用 doNotify 方法,doWait 方法会一直自旋,CPU 会消耗太大)。被唤醒的线程会自旋直到自旋锁(while 循环)里的条件变为 false。以下 MyWaitNotify2 的修改版本展示了这点:
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 wait()方法是在 while 循环里,而不在 if 表达式里。如果等待线程没有收到信号就唤醒,wasSignalled 变量将变为 false,while 循环会再执行一次,促使醒来的线程回到等待状态。
多个线程等待相同信号
如果你有多个线程在等待,被 notifyAll()唤醒,但只有一个被允许继续执行,使用 while 循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出 wait()调用并清除 wasSignalled 标志(设为 false)。一旦这个线程退出 doWait()的同步块,其他线程退出 wait()调用,并在 while 循环里检查 wasSignalled 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。
不要在字符串常量或全局对象中调用 wait()
(校注:本章说的字符串常量指的是值为常量的变量)
本文早期的一个版本在 MyWaitNotify 例子里使用字符串常量(””)作为管程对象。以下是那个例子:
public class MyWaitNotify{ String myMonitorObject = ""; boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
在空字符串作为锁的同步块(或者其他常量字符串)里调用 wait()和 notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有 2 个不同的 MyWaitNotify 实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个 MyWaitNotify 实例上调用 doWait()的线程会被在第二个 MyWaitNotify 实例上调用 doNotify()的线程唤醒。这种情况可以画成以下这张图:
起初这可能不像个大问题。毕竟,如果 doNotify()在第二个 MyWaitNotify 实例上被调用,真正发生的事不外乎线程 A 和 B 被错误的唤醒了 。这个被唤醒的线程(A 或者 B)将在 while 循环里检查信号值,然后回到等待状态,因为 doNotify()并没有在第一个 MyWaitNotify 实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程 A 或者 B 在信号值没有更新的情况下唤醒。但是代码处理了这种情况,所以线程回到了等待状态。记住,即使 4 个线程在相同的共享字符串实例上调用 wait()和 notify(),doWait()和 doNotify()里的信号还会被 2 个 MyWaitNotify 实例分别保存。在 MyWaitNotify1 上的一次 doNotify()调用可能唤醒 MyWaitNotify2 的线程,但是信号值只会保存在 MyWaitNotify1 里。
问题在于,由于 doNotify()仅调用了 notify()而不是 notifyAll(),即使有 4 个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程 A 或 B 被发给 C 或 D 的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而 C 和 D 都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C 和 D 被发送过信号,只是都不能对信号作出回应。
doNotify() 메소드가 inform() 대신에 informAll()을 호출하면 대기 중인 모든 스레드가 깨어나고 신호 값을 차례로 확인합니다. 스레드 A와 B는 대기 상태로 돌아가지만 단 하나의 스레드, C 또는 D만 신호를 확인하고 doWait() 메서드 호출을 종료합니다. C 또는 D의 다른 하나는 신호를 받은 스레드가 doWait() 종료 시 신호 값을 삭제(false로 설정)했기 때문에 대기 상태로 돌아갑니다.
위 문단을 읽은 후, inform() 대신에 informAll()을 사용해 볼 수도 있겠지만 이는 성능 측면에서 좋지 않은 생각이다. 단 하나의 스레드만 신호에 응답할 수 있는 경우 매번 모든 스레드를 깨울 이유가 없습니다.
따라서 wait()/notify() 메커니즘에서는 전역 개체, 문자열 상수 등을 사용하지 마세요. 해당 고유 개체를 사용해야 합니다. 예를 들어 빈 문자열에 대해 wait()/notify()를 호출하는 대신 MyWaitNotify3의 각 인스턴스에는 자체 모니터 개체가 있습니다.
위 내용은 Java 멀티스레딩 및 스레드 통신에 대한 정보를 모아 놓은 것입니다. 앞으로도 관련 정보를 계속 추가하겠습니다.
더 많은 Java 멀티스레딩-스레드 통신 예제 및 관련 기사를 보려면 PHP 중국어 웹사이트를 주목하세요!