前言
本来这章应该来讨论Java的内存模型,但我的博客里其实已经有了一篇,同样的内容再写一遍觉得怪怪的,所以这里就直接到线程之间的通信吧,如果有人需要看的话,连接在这里《深入理解JVM》,写得比较渣,其实都是为了加深自己记忆的,所以就比较敷衍。看不下去的自己找几篇看看问题不大,毕竟其实大家看的书差不多那内容也就差不多。
线程通信
线程通信的目的是为了使线程之间能够发送信号,也使得线程能够等待其它线程的信号。
1.1 通过共享对象通信
这是线程之间进行通信的一个最简单的方式,通过在共享对象的变量上设置信号值。
public class MySignal {
public boolean signal=false;
public synchronized boolean getSignal(){
return this.signal;
}
public synchronized void setSignal(boolean signal) {
this.signal = signal;
}
}
以上是我们定义的信号类,其中定义了一个成员变量signal,线程A可以在一个同步块中通过setSignal()
方法来将signal设置为true
,线程B则能够在一个同步块中获得signal的值,线程A、B必须获得指向一个MySignal
实例的引用,然后它们才能够进行通信,否则无法检测到彼此的信号。
1.2 忙等待(Busy waiting)
表示一个线程正在等待另一个线程的信号,该信号可以使得数据变为可用,
while(!sharedSignal.hasDataToProcess()){
//do nothing... busy waiting
}
1.3 wait()/notify()/notifyAll()
上述的忙等待并没有很好地利用CPU,除非平均等待的时间很短,否则,更为明智的选择应该是将线程置于睡眠或者其它非运行状态,直到它接收到需要的信号。
Java有一个内建的机制来使得线程在等待的时间变为非运行状态,java.lang.Object有三个方法:wait()
/notify()
/notifyAll()
用来实现这个机制。
一个线程如果调用了任意对象的wait()
方法,就会进入非运行状态,直到另一个线程调用了同一个对象的notify()
方法,为了调用wait()
和notify()
方法就需要获得对象的锁,因此,线程必须在同步代码块里调用wait()
和notify()
方法,以下为示例:
public class MyWaitNotify {
MonitorObj monitorObj=new MonitorObj();
//令线程等待
public void doWait(){
synchronized (monitorObj){
try {
monitorObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//唤醒线程
public void doNotify(){
synchronized (monitorObj){
monitorObj.notify();
}
}
}
需要注意的是,当一个线程调用notify()
方法时,将会使得所有在等待该对象的线程中的一个被唤醒并允许执行,这个被唤醒的线程是随机的,我们无法指定。当然,我们也可以使用notifyAll()
来唤醒所有等待该对象的线程。
我们需要铭记:无论是将线程置于等待或者唤醒线程,wait()
和notify()
方法都必须在同步块中调用,这是强制性的,如果一个对象没有持有对象锁,将不能调用wait()和notify()方法,否则会抛出IllegalMonitorStateException
。这是由于JVM中,调用某个对象的wait()方法时会首先检查当前线程是否是对象锁的拥有者。
这里我们可能会有疑问,线程在执行doWait()
方法时就会进入同步块中,这个过程里就会一直持有监视器对象(即)的锁,这会不会阻塞线程进入notify()
的同步块。实际上是不会的,线程在调用对象的wait()
方法时,就会释放所持有的监视器对象的锁。这样其他线程就能够调用该对象的wait()和notify()。
只有在执行notify()
方法的线程退出同步块之后,被唤醒的线程才能退出wait()
方法,这说明:线程被唤醒必须要重新获得监视器对象的锁。如果使用了notifyAll()
方法,那么同一时刻只有一个线程可以退出wait()
方法,因为每个线程在退出wait()
方法前都需要获得锁。
1.4 丢失的信号(Missed Signals)
notify()和notifyAll()不会保存调用它们的方法,因为当这两个方法被调用时,有可能并没有线程处在等待状态,通知信号之后会被丢弃。因此,如果一个线程先于被通知线程调用wait()前调用了notify(),被通知线程就会丢失这个信号。这可能导致某些线程丢失它的唤醒信号,处在永久的等待状态。
为了避免信号丢失,我们可以将它们保存在信号类里,在 MyWaitNotify 的例子中,通知信号应被存储在 MyWaitNotify 实例的一个成员变量里。我们对它进行如下的修改:
public class MyWaitNotify {
MonitorObj monitorObj = new MonitorObj();
//使用一个变量来指示对象是否被通知过
boolean wasSignalled = false;
public void doWait() {
synchronized (monitorObj) {
if (!wasSignalled){
try {
monitorObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//clear signal and keep running
wasSignalled=false;
}
}
public void doNotify() {
synchronized (monitorObj) {
wasSignalled=true;
monitorObj.notify();
}
}
}
在doNotify()
方法中对象的notify()
之前,先行将wasSignalled
置为true,在doWait()
中调用对象的wait()
前先行检查wasSignalled()
,以上的步骤总结起来就是:在notify()
之前,设置自己已经被通知过了,在wait()
之后设置自己未被通知过,正在等待通知。
1.5 假唤醒(Spurious wakeups)
由于不可知的原因,线程可能在没有调用notify()
或者notifyAll()
的情况下就被唤醒了,这就是所谓的假唤醒。假设线程在wait()
状态下被假唤醒了,并继续执行下去,这可能导致我们的程序出现严重的问题。
为了防止假唤醒,我们可以将检查wasSignalled
标识的代码从if表达式中换成一个while循环,这样的一个while循环可以称为自旋锁。但我们要慎重地使用自旋锁,这是因为,如果线程长时间地处于doWait()
的,那么doWait()
方法就会一直自旋,而JVM中自旋的实现会消耗CPU资源。线程会自旋到while的条件表达式变为false,在上例中就是wasSignalled
变为true,这时自旋锁才会中止。我们继续对MyWaitNotify 例程进行修改:
public class MyWaitNotify {
MonitorObj monitorObj = new MonitorObj();
boolean wasSignalled = false;
public void doWait() {
synchronized (monitorObj) {
//此处的if表达式变为while
while (!wasSignalled){
try {
monitorObj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//clear signal and keep running
wasSignalled=false;
}
}
public void doNotify() {
synchronized (monitorObj) {
wasSignalled=true;
monitorObj.notify();
}
}
}
如果等待线程没有收到唤醒信号就启动,那么,由于wasSignalled
依然是false,那么while循环只要再执行一遍,就会将线程重新阻塞,回到等待状态。
1.6 多个线程等待相同信号
如果有多个线程处于等待状态,程序使用了notifyAll()
进行唤醒,其中只有一个能够被允许继续执行,上一节中的自旋锁是一个很好的解决方案。每次只有一个线程可以获得监视器对象锁,意味着每次只有一个线程能够退出wait()
,并将wasSignalled
重置为false。
1.7 不要在字符串常量或者全局变量中调用wait()
使用常量作为管程对象(监视器对象,用于实现共享资源的互斥访问)的麻烦在于,JVM中,同样的字符串常量会被当做同一个对象,这就意味着我们就算新建了两个对象实例,它们的管程对象仍然是同一个。由此引发的问题是,当AB两个线程分别持有一个对象实例时,A线程调用持有的一个实例的wait()
方法时可能被B线程中的另一个对象的notify()
方法唤醒。当然,如果我们为对象实现了自旋锁,那么在执行doWait()
方法时,while循环中会检查对象的信号值,然后使得线程重新回到等待状态。
但这么做之后又引发了新的问题:我们假设有A、B、C、D四个线程在同一个字符串管程对象上等待,只有一个能够被唤醒。如果被A或B发送给C或D的信号唤醒,那么它们就会在while循环中检查wasSignalled
的状态,然后继续保持wait状态。但C和D都没有被唤醒来检查信号,这样就出现了信号丢失的问题。
所以: 在 wait()
/notify()
机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象。