欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程通信

程序员文章站 2022-04-30 09:09:22
...

线程通信

  • 线程通信的目标就是使线程互相发送信号,也可以使线程能够等待其他线程的信号

通过共享对象通信

  • 设置信号量:

    • 线程A在一个同步块里设置Boolean型的变量,线程B也在同步块里进行读取
    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共享实例的引用,以便于通信。

  • 需要处理的数据可以存放在一个共享缓存区里,它和Mysignal实例是分开存放的

忙等待

  • 准备处理数据线线程B正在等待数据变为可用,也就是它在等待线程A的一个信号,这个信号使hasDataToProcess()返回true,线程B运行在一个循环中,等待这个信号

    protected MySignal sharedSignal = ...;
        ...
        while(!sharedSignal.hasDataToProcess()){
            //do nothing... busy waiting
        }
    

wait(),notify()和notifyAll()

  • 忙等待的缺点就是没有对运行等待线程B的CPU进行有效的利用,所以这里让线程进入睡眠或者非运行状态,直到它接收到它等待的信号

  • java中的wait(),notify()和notifyAll()可以实现这个等待机制

  • 一个线程一旦调用了任意对象的wait()方法,就会变为非运行时状态,直到另一个线程调用了同一个对象的notify()方法,线程需先获得目标对象的锁。即线程必须在同步块里调用wait()或者notify()方法

    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()。当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程被唤醒并允许执行。但notifyAll()方法来唤醒正在等待一个给定对象的所有线程

  • 不管是等待线程还是唤醒线程都在同步块里调用wait()金额mptify()。一个线程如果没有持有对象锁,将不能调用wait()、notify()、notifyAll()、否则将会抛出IllegalMonitorStateException异常

  • 旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。

丢失的信号(Missed Signals)

  • 由于notify()和notifyAll()方法不会保存调用它们的方法,因为当这两个方法被调用时,有可能没有线程处于等待状态。通知信号过后便丢弃了。
  • 如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。
  • 为了避免丢失信号,必须把它们保存在信号类里。
public class MyWaitNotify {
    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()和notifyAll()的情况下醒来。这就是所谓的假唤醒。
  • 如果在MyWaitNotify的doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。
  • 为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(但是JVM实现自旋会消耗CPU)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。
public class MyWaitNotify {
    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循环中,如果等待线程没有收到信号就唤醒,wassignalled变量变为false,while循环会在执行一次,促使醒来的线程回到等待状态

多个线程等待相同信号

  • 如果多个线程在等待,被notifyAll()唤醒,但只有一个被允许继续执行
  • 使用while循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出wait()调用并清除wasSignalled标志(设为false)。
  • 一旦这个线程退出doWait()的同步块,其他线程退出wait()调用,并在while循环里检查wasSignalled变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。

不要在字符串窗帘或全局对象中调用wait()

  • 在空字符串作为锁的同步块里调用wait()和notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。
  • 这意味着,即使你有2个不同的MyWaitNotify实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个MyWaitNotify实例上调用doWait()的线程会被在第二个MyWaitNotify实例上调用doNotify()的线程唤醒(假唤醒)。
  • 由于doNotify()仅调用了notify()而不是notifyAll(),即使有4个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程A或B被发给C或D的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而C和D都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C和D被发送过信号,只是都不能对信号作出回应。
  • 如果doNotify()方法调用notifyAll(),而非notify(),所有等待线程都会被唤醒并依次检查信号值。线程A和B将回到等待状态,但是C或D只有一个线程注意到信号,并退出doWait()方法调用。C或D中的另一个将回到等待状态,因为获得信号的线程在退出doWait()的过程中清除了信号值(置为false)。
  • 如果使用notifyAll()来代替notify(),但是这在性能上是个坏主意。在只有一个线程能对信号进行响应的情况下,没有理由每次都去唤醒所有线程。所以:在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象。例如,每一个MyWaitNotify3的实例(前一节的例子)拥有一个属于自己的监视器对象,而不是在空字符串上调用wait()/notify()。
相关标签: 所有文章 Java