Thread详解8:wait/notify机制

我通过一个问题来引入这篇博文里要介绍的知识点。现在有这样一个需求,内存中有一个容器,容量为5,线程 AddOne 每隔一段时间就往这个容器里反复一个单位的货物,放11个就结束。线程 Supervisor 就是用来监控这个容器的,一旦容器满了,它就将这个容器清空。你的解决方案是什么?

1 利用while循环

AddOne.java

package medium2;

import java.util.ArrayList;

public class AddOne extends Thread {
    private ArrayList<Integer> aList;

    public AddOne(ArrayList<Integer> aList) {
        super("AddOne");
        this.aList = aList;
    }

    @Override
    public void run() {
        super.run();
        // 往容器中放11个货物
        for (int i = 0; i < 11; i++) {
            aList.add(i);
            System.out.printf(Thread.currentThread().getName() + " puts %d-th object into the container!\n", i + 1);
            // 每隔1秒放一个
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 完成任务后退出
        System.out.println(Thread.currentThread().getName() + " finished!");
        System.exit(NORM_PRIORITY);
    }
}

Supervisor.java

package medium2;

import java.util.ArrayList;

public class Supervisor extends Thread {
    private ArrayList<Integer> aList;

    public Supervisor(ArrayList<Integer> aList) {
        super("Supervisor");
        this.aList = aList;
    }

    @Override
    public void run() {
        super.run();
        while (true) {
            // 一旦监测到容器已经满了就将其清空
            if (aList.size() == 5) {
                aList.clear();
                System.out.println(Thread.currentThread().getName() + " has emptied the container");
            } else {
                System.out.println("The size is " + aList.size());
            }

            // 每隔300ms检查一次
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ArrayList<Integer> aList = new ArrayList<>();
        AddOne thread1 = new AddOne(aList);
        Supervisor thread2 = new Supervisor(aList);
        thread1.start();
        thread2.start();

    }

}

输出

The size is 0
AddOne puts 1-th object into the container!
The size is 1
The size is 1
The size is 1
AddOne puts 2-th object into the container!
The size is 2
The size is 2
The size is 2
AddOne puts 3-th object into the container!
The size is 3
The size is 3
The size is 3
AddOne puts 4-th object into the container!
The size is 4
The size is 4
The size is 4
The size is 4
AddOne puts 5-th object into the container!
Supervisor has emptied the container
The size is 0
The size is 0
AddOne puts 6-th object into the container!
The size is 1
The size is 1
The size is 1
AddOne puts 7-th object into the container!
The size is 2
The size is 2
The size is 2
The size is 2
AddOne puts 8-th object into the container!
The size is 3
The size is 3
The size is 3
AddOne puts 9-th object into the container!
The size is 4
The size is 4
The size is 4
AddOne puts 10-th object into the container!
Supervisor has emptied the container
The size is 0
The size is 0
The size is 0
AddOne puts 11-th object into the container!
The size is 1
The size is 1
The size is 1
AddOne finished!

上面这种做法的缺点在于:Supervisor按照既定的时间间隔去扫描容器状态,这样会消耗CPU资源;如果轮询的时间间隔很小,会更消耗CPU资源;如果轮询时间间隔过大,有可能错过了时机,造成了错误。比如容器已经满了,还往里面塞东西,结果溢出。

有没有一种更好的解决方案呢? 我在这里就不卖关子了,有的,常见的解决方案就是【等待/通知】机制。AddOne自己放货物到容器的时机就可以随便关注一下一下容器的状态,如果它发现容器已经满了,就通知Supervisor你把容器清空吧,我下次还要用。这种机制在Java中就对应与两个方法:wait和notify。下面我对它们的使用做一个入门级的介绍。



2 wait / notify

首先强调的一点是,这两个方法不在Thread下,而在java.lang.Object类下,换句话说,所有的类都有这两个方法。

这里写图片描述

另外,关于等待/通知,要记住的关键点是:

  1. 必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
  2. 与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该对象的通知。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到得到对象的notify()信号为止。
  3. 如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。

下面我用通知等待机制编写Section 1中提出的问题的解决方案。

AddOne2.java

package medium2;

import java.util.ArrayList;

public class AddOne2 extends Thread {
    private ArrayList<Integer> aList;

    public AddOne2(ArrayList<Integer> aList) {
        super("AddOne2");
        this.aList = aList;
    }

    @Override
    public void run() {
        super.run();
        synchronized (aList) {
            for(int i=0; i<11; i++){
                aList.add(i);
                System.out.printf(Thread.currentThread().getName() + " puts %d-th object into the container!\n", i + 1);
                // 每隔1秒放一个
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(aList.size() == 5){
                    aList.notify();
                    System.out.println("AddOne2 已经通知 Supervisor 了");
                }
            }
        }
    }
}

Supervisor2.java

package medium2;

import java.util.ArrayList;

public class Supervisor2 extends Thread {
    private ArrayList<Integer> aList;

    public Supervisor2(ArrayList<Integer> aList) {
        super("Supervisor");
        this.aList = aList;
    }

    @Override
    public void run() {
        super.run();
        System.out.println("Supervisor has started running");
        synchronized (aList) {
            System.out.println("Supervisor has got the lock");
            try {
                // 等待通知
                aList.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            aList.clear();
            System.out.println(Thread.currentThread().getName() + " has emptied the container");

        }
    }

    public static void main(String[] args) {
        ArrayList<Integer> aList = new ArrayList<>();
        Supervisor2 thread2 = new Supervisor2(aList);
        thread2.start();
        // sleep是为了保证 Supervisor2 先wait,不然AddOne先notify了,wait就没有意义
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        AddOne2 thread1 = new AddOne2(aList);
        thread1.start();

    }

}

输出

Supervisor has started running
Supervisor has got the lock
AddOne2 puts 1-th object into the container!
AddOne2 puts 2-th object into the container!
AddOne2 puts 3-th object into the container!
AddOne2 puts 4-th object into the container!
AddOne2 puts 5-th object into the container!
AddOne2 已经通知 Supervisor 了
Supervisor has emptied the container

咦!怎么回事!?输出和预想的不一样!看来上面的代码有点问题,但是这个代码的意义比正确的代码更重要!因为好好分析一下,可以发现很多重要的性质。

  • 首先,最先运行的是Supervisor,它先获得了aList对象的锁,然后执行到wait,立刻就放弃了锁,这样AddOne才可以执行同步代码块内的代码。
  • AddOne添加5个货物后就通知了Supervisor,可是从输出我们看出,AddOne还在继续执行,直到AddOne的同步代码块内的代码执行完,Supervisor才执行,也才输出了 “Supervisor has emptied the container”,而这个时候容器早就爆了。
  • 另外一个值得注意的地方就是,在执行AddOne的for循环中我用了sleep,可是,即使在AddOne sleep的时间内,被唤醒的Supervisor还是没能得到锁,没能执行,为什么? 这也证明了sleep虽然让线程进入了阻塞状态,但是即使在sleep,该线程也不会释放所持有的锁,这一知识点很重要。

所以我再来补充强调Java 等待/通知 机制的几个特性:

  1. wait之后,该线程会立即释放同步Object的锁,并转入阻塞状态。
  2. 但是,一个线程调用了notify之后,并不会立刻放弃同步Object的锁,而是继续执行同步代码块内的代码。又因为都加了synchronized,线程串行执行,所以只有当这个线程执行完同步代码块内的代码,被成功唤醒的线程才能执行。
  3. 一个notify只能随机唤醒一个wait的线程(这个提一下,就不写代码证明了)

这问题分析完了,可上面的问题还没有解决啊。为了一篇博文的篇幅不要过长,这篇博文我就写到这里。下一篇博文直接介绍【消费者/生产者模式】,可以满足此处提出的需求,也是等待/通知机制的经典应用案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值