使用wait/notify实现线程间的通信

之前对Java多线程中的wait/notify机制理解都不是很清晰,最近看了一本技术书,通过里面的讲解再配上一些博客,终于算是对wait/notify有了进一步的理解。

下面就来说说我对这两个方法的认识:

都知道在Java中,我们可以通过继承Thread或者实现Runnable接口来实现多线程,这些线程会各自执行自己的任务,但是一个人的力量是有限的,一个线程的力量也是有限的,要想使系统各部分配合得更好,我们需要实现各个线程间的通信。要实现线程间的通信最好的方法就是使用wait/notify机制。

这里有两个新问题:

  1. 什么是线程间的通信?
  2. 如何使用wait/notify机制实现线程间的通信?

为了解决这两个问题,我们来看看下面这段代码:

public class ThreadA extends Thread{
    private MyList list;
    public ThreadA(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            list.add();
            System.out.println("第  "+i+" 此执行add操作");
        }
    }
    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        a.start();
        b.start();
    }
}

class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {

        while( true ) {
            if (list.size() == 3) {
                System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");
                Thread.interrupted();
            }
        }
    }
}
class MyList{
    private ArrayList<String> list = new ArrayList<String>();
    public void add() {
        list.add("abc");
    }
    public int size() {
        return list.size();
    }
}

执行结果如下:

第 0 此执行add操作
第 1 此执行add操作
第 2 此执行add操作
线程Thread检测到list中的长度变为3了,停止该线程
第 3 此执行add操作
第 4 此执行add操作

在上面的例子中实现了两个线程,其中线程ThreadA的任务是向list中添加值,而线程ThreadB的任务是监听list中的数量,一旦list的长度达到了3,就通过interrupted方法停止该线程的监听操作。

在这里线程ThreadA与线程ThreadB就实现了通信,这个例子中的通信指的就是:线程ThreadA执行向list添加值的操作,而线程ThreadB通过不断的执行while监听来观察线程ThreadA的操作,从而做出自己相应的操作。可是这种通信方式有一个弊端,线程ThreadB需要不断的执行while循环来达到监听的目的,这样对CPU的资源会产生浪费。服务端的资源是非常宝贵的,所以就出现了wait/notify机制来解决这个问题。

我们看看使用wait/notify机制后,上面的线程间通信会变成什么样?


public class ThreadA extends Thread{
    private MyList list;
    public ThreadA(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (list) {
            for (int i = 0; i < 5; i++) {
                list.add();
                System.out.println("第  "+i+" 此执行add操作");
                    try {
                        if (list.size() == 3) 
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
        }
    }
    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        a.start();
        b.start();
    }
}
class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (list) {
            System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");    
            list.notify();  
            System.out.println("notify方法调用结束");
        }
    }
}
class MyList{
    private ArrayList<String> list = new ArrayList<String>();
    public void add() {
        list.add("abc");
    }
    public int size() {
        return list.size();
    }
}

执行结果如下:

第 0 此执行add操作
第 1 此执行add操作
第 2 此执行add操作
线程Thread检测到list中的长度变为3了,停止该线程
notify方法调用结束
第 3 此执行add操作
第 4 此执行add操作

上面是使用了wait/notify机制后的代码实现,两种方式的执行结果相同。但是代码的实现方式却大有不同,可以看到线程ThreadB里没有了while循环,说明在该线程里不用一直监听list中的值了,所以资源浪费的问题得到了解决,这其中的原理是什么呢?

首先需要注意的一点是,wait/notify方法必须存在于同步代码块里,因为这两个方法需要由锁对象去调用,在上面的例子中锁对象是list,调用wait与notify的方法都是同一个锁对象,具体用意在下面再讲。

然后我们来仔细说说代码的执行流程,首先我们先启动的是a线程,a线程拿到锁对象list,进入该线程run方法中的同步代码块,执行for循环,for循环中一直执行add添加操作,并进行if判断,当执行到第三次的时候进入if判断,执行list.wait(),当执行完这一句代码,当前线程立即释放list锁对象,由于此时,b线程也在等待同一把锁对象,所以该线程的拿到锁对象后执行对应的同步代码块内的内容。

当执行完list.notify()后,线程b会唤醒线程a,使其处于runnable状态,一旦抢到锁对象后立即接着list.wait()后的代码执行,与list.wait()不同的是,线程b在执行完list.notify()后没有立即释放锁,而是在执行完同步代码块中的所有内容后,锁才会被释放,锁被释放后,由于线程a已经被notify唤醒,所以会接着上次的地方执行。


在使用等待/唤醒机制的时候,有几个需要注意的地方。

  • 一个notify()方法只会唤醒一个处于等待中的线程,要想唤醒所有,可以使用notifyAll()方法
  • notify()方法只会唤醒被同一个锁对象wait()的线程。

对于第二点,如果我们将ThreadB修改成这样:

class ThreadB extends Thread{
    private MyList list;
    public ThreadB(MyList list) {
        this.list = list;
    }
    @Override
    public void run() {
        synchronized (this) {
            System.out.println("线程Thread检测到list中的长度变为3了,停止该线程");
            this.notify();
            System.out.println("notify方法调用结束");
        }
    }
}

执行结果如下,并且程序一直没有停止,可见线程a没有被唤醒。

第 0 此执行add操作
第 1 此执行add操作
第 2 此执行add操作
线程Thread检测到list中的长度变为3了,停止该线程
notify方法调用结束

这是因为调用notify与wait方法不是使用的同一个锁对象,可以理解为这两个notify和wait不是一对,所以没达到唤醒的目的。


总结

经过对比发现,我发现wait/notify机制更像是一种主动推送的实现,一旦A线程达到了某种状态,可以通过wait方法来通知与A线程使用同一个锁对象的线程,让他们去做相应的操作,由于只有一把锁,所以只能有一个线程获得执行机会,这个获得执行机会的线程肯定是在A线程中达到某种状态后才会执行,所以也就间接实现了通信的目的。

猜你喜欢

转载自blog.csdn.net/KKSJS/article/details/81087138