[Java 并发编程] 15. 线程通信

文章目录

  • 一、通过共享对象实现线程通信
  • 二、Busy Wait
  • 三、等待通知机制:wait()、notify()、notifyAll()
  • 四、丢失信号
  • 五、虚假唤醒
  • 六、当多个线程等待同一个信号


一、通过共享对象实现线程通信

线程通过在共享对象中发送一个信号实现与其他线程通信。如下图所示,设置一个成员变量 hasProcess,线程A通过setHasProcess同步方法设置hasProcess的值,这样线程B可以读取到成员变量hasProcess的值,实现线程之间的通信。

public class MySignal {

    private boolean hasProcess = false;

    public synchronized void setHasProcess(boolean hasProcess) { this.hasProcess = hasProcess; }

    public synchronized boolean getHasProcess() { return this.hasProcess; }    

}

线程A和线程B必须引用共享对象 MySignal 的实例来实现线程A与线程B之间的线程通信。如果线程A和线程B引用了不同的MySignal实例对象,那么线程A和线程B不会检测到彼此发送的信号,达不到线程通信的目的。


二、Busy Wait

上例中线程B需要等待信号处理数据,因此它的业务代码很可能一直在等待信号,浪费了很多CPU资源。

MySignal singal = ...

while (!singal.getHasProcess()) {
    //do nothing... busy waiting
}

因为我们不知道线程A什么时候发送信号,因此导致线程B一直等待线程A的信号,没做任何事情,浪费CPU资源。


三、等待通知机制:wait()、notify()、notifyAll()

Busy waiting 浪费了CPU资源,即使某些情况下等待的时间非常短暂。因此,让等待信号的线程在接收信号之前处于无效状态,直到接收信号之后(被唤醒)继续执行代码,这是一种非常聪明的做法。

**等待通知机制:**让等待信号的线程在接收信号之前处于无效状态(不占用CPU资源,线程暂停执行),由发出信号的线程唤醒等待信号的线程,等待信号的线程被唤醒后继续执行,这就是等待通知机制。

Java 有一个等待机制:让等待信号的线程处于无效状态。Java Object 类定义了三个方法分别是wait()、notify()、notifyAll(),通过这三个方法我们可以实现这种机制。

一个线程调用了某个对象的 wait() 方法之后,这个线程就成为无效状态,直到另外一个线程调用了同一个对象的 notify() 方法或者 notifyAll()方法之后,原来处于无效状态的线程才结束无效状态。

线程调用某个对象的 wait() 方法或者 notify() 方法必须获取这个对象的锁,也就是说 wait() 方法或者 notify() 方法调用的代码必须包含在 synchronized 代码块中,synchronized 监视器必须是 wait() 或 nofity() 方法所属的那个对象。

示例:

//监视器对象
public class MonitorObject {
}

public class MyWaitNotify {

    MonitorObject myMonitorObject = new MonitorObject();

    public void doWait() {
        synchronized (myMonitorObject) {
            try {
                //wait()方法必须包含在synchronized代码块中,且synchronized监视器是wait()方法所属对象myMonitorObject
                myMonitorObject.wait();
            } catch (InterruptedException e) {
                ...
            }
        }
    }

    public void doNotify() {
        synchronized (myMonitorObject) {
            //notify()方法必须包含在synchronized代码块中,且synchronized监视器是notify()方法所属对象myMonitorObject
            myMonitorObject.notify();
        }
    }
}

当一个线程调用某个对象的notify()方法,会唤醒一个等待状态的线程;通过调用notifyAll()方法,唤醒所有处于等待状态的线程。

你可以从上例中观察到wait()方法和notify()方法都在同步代码块中,并且同步代码块的监视器对象与调用wait方法(或notify方法)的对象是同一个对象。这是强制的,一个线程不能在没有持有某个对象锁的方法上调用wait方法或notify方法,否则程序将抛出 IllegalMonitorStateException (非法监视器状态异常)。

你可能会想:当等待线程进入synchronized代码块调用wait方法使得线程进入等待状态,并没有退出synchronized代码块,那么等待线程就会阻止唤醒线程进入synchronized代码块调用notify()方法,那么唤醒线程怎么可能能够进入synchronized代码块呢?答案是唤醒线程可以进入synchronized代码块调用notify方法,原因是当一个线程调用了wait方法之后,当前线程会释放基于某个对象的持有锁,这样其他的线程就有机会进入synchronized代码块。

一旦一个线程被唤醒,它不能立即退出wait()方法,等待线程需要等待唤醒线程退出包含notify()方法的synchronized代码块之后,等待线程需要重新获取对象的持有锁之后退出wait方法,然后继续执行下面的代码。

使用notifyAll()唤醒多个等待线程,那么多个等待线程也需要等唤醒线程退出synchronized代码块之后,各个等待线程需要重新获取对象的持有锁才能退出wait()方法继续执行程序,由于各个等待线程的synchronized代码块的监视器是同一个对象,因此各个线程之间是同步退出wait()方法。

小结:

  • Java 的等待通知机制是基于某个对象实现的一种等待通知机制。
  • Java Object 类定义的 wait()、notify()、notifyAll()用于线程通信,这些方法的调用必须包含在以这些方法的对象为监视器的synchronized代码块中,否则程序将抛出IllegalMonitorStateException异常。
  • 当一个线程调用wait()方法后,线程会释放基于这个方法的对象的持有锁,并使线程进入无效状态,只有当其他线程调用了同一个对象的notify方法(或notifyAll方法)之后,这个对象才可能被唤醒,等待线程需等唤醒线程退出包含notify方法(或notifyAll方法)的synchronized代码块之后,才能重新获取对象的持有锁并退出wait方法继续执行程序。
  • 当一个线程调用某个对象的notifyAll方法后,所有基于这个对象的等待中的线程将全部被唤醒,但这些等待中的线程需要等唤醒线程退出包含notifyAll方法的synchronized代码块之后,同步的获取对象的持有锁并退出wait()方法。

四、丢失信号

当一个线程调用notify方法时没有线程处于等待状态,那么这个唤醒信号就被丢失了。丢失信号可能会导致程序产生一些问题,也可能不会产生一些问题,但我们需要知道这种情况可能发生。在某些情况下,丢失信号可能导致一些等待线程一直处于等待中,从未被唤醒,因为唤醒线程调用notify方法发生在等待线程调用wait方法之前。

为了避免这个问题,我们可以将信号保存在共享数据对象中,这里就不在举例了。


五、虚假唤醒

请看示例:

/**
 * 程序入口 
 *
 * @author : sungm
 * @date : 2020-09-01 17:00
 */
public class Main {

    public static int number = 0;
    
    public static void main(String[] args) {
        Lock lock = new Lock();
        
        Runnable myRunnable = () -> {
            try {
                lock.lock();
                number++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unLock();
            }
        };
        
        new Thread(myRunnable, "Thread A").start();
        new Thread(myRunnable, "Thread B").start();
        new Thread(myRunnable, "Thread C").start();
    }

}
/**
 * 自定义锁
 *
 * @author : sungm
 * @date : 2020-09-01 16:21
 */
public class Lock {

    private boolean hasLocked = false;

    public synchronized void lock() throws InterruptedException {
        if (hasLocked) {
            wait();
        }
        hasLocked = true;
    }

    public synchronized void unLock() {
        hasLocked = false;
        notify();
    }

}

现在我们来分析下上面代码中可能会存在什么问题?

首先假设线程A调用了lock()方法获取到了锁,然后执行number++操作时,此时线程B进入lock()方法(锁已被线程A获取),因此线程B调用wait()方法进入等待状态,之后线程A调用了unLock()方法释放锁资源并唤醒一个线程,因为这里只有线程B处于wait状态,因此线程B被唤醒,线程B等待重新进入synchronized代码块,若此时线程C优先于线程B进入lock同步方法,锁归线程C所有,当线程C退出lock方法后,线程B进入synchronized退出wait方法,继续执行下面的程序代码,那么此时锁同时被线程B和线程C拥有,出现了不同步的操作,这样容易导致程序出现问题。

现在我们来解决上面代码存在的问题

我们只要将lock()方法中的if(hasLocked)判断条件换成while(hasLocked)代码块,就能很好的解决上面这个问题,while(hasLocked)我们称它为“自旋锁”。如下所示:

public synchronized void lock() throws InterruptedException {
    while (hasLocked) {
        wait();
    }
    hasLocked = true;
}

这样,当线程B退出wait()方法后继续判断hasLocked条件是否为真,仅当锁没有被任何线程锁拥有时才真正的唤醒线程,否则线程继续等待。


六、当多个线程等待同一个信号

自旋锁在处理多个线程等待同一个信号时也是一种很好的方案,我们会使用notifyAll()方法唤醒等待的所有线程,同时只有一个线程能够退出wait()方法,当某个线程退出wait()方法后,这个线程会修改hasLocked的值,当其他线程退出wait方法时会自旋判断hasLocked,若锁被其他线程拥有,会继续进入等待状态,从而避免程序产生一些不正常的操作。

你可能感兴趣的:(Java,并发编程,java,并发编程,多线程)