我通过一个问题来引入这篇博文里要介绍的知识点。现在有这样一个需求,内存中有一个容器,容量为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类下,换句话说,所有的类都有这两个方法。
另外,关于等待/通知,要记住的关键点是:
- 必须从同步环境内调用wait()、notify()、notifyAll()方法。线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁。
- 与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该对象的通知。线程通过执行对象上的wait()方法获得这个等待列表。从那时候起,它不再执行任何其他指令,直到得到对象的notify()信号为止。
- 如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。
下面我用通知等待机制编写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 等待/通知 机制的几个特性:
- wait之后,该线程会立即释放同步Object的锁,并转入阻塞状态。
- 但是,一个线程调用了notify之后,并不会立刻放弃同步Object的锁,而是继续执行同步代码块内的代码。又因为都加了synchronized,线程串行执行,所以只有当这个线程执行完同步代码块内的代码,被成功唤醒的线程才能执行。
- 一个notify只能随机唤醒一个wait的线程(这个提一下,就不写代码证明了)
这问题分析完了,可上面的问题还没有解决啊。为了一篇博文的篇幅不要过长,这篇博文我就写到这里。下一篇博文直接介绍【消费者/生产者模式】,可以满足此处提出的需求,也是等待/通知机制的经典应用案例。