Java并发编程(3)——线程通信

前言

本来这章应该来讨论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()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象。

猜你喜欢

转载自blog.csdn.net/ascend2015/article/details/80386374