最近项目中遇到了一个特殊的问题,需要使用异步的方式完成某个小任务,还要求异步任务完成之后返回的结果集,由发起任务的线程继续去处理。这样的状况用回调的方式去处理,需要切换线程,可能不太合适。笔者想到这个问题应该比较类似于操作系统里面的生产者消费者问题,就决定借鉴一下生产者消费者问题的解决思路。线程A和线程B共享同一块内存区,线程A缺少执行锁需要的资源时,向线程B发送消息,让线程B去获取资源,发完消息之后,线程A进入阻塞状态,线程B得到消息之后,从阻塞态变为执行状态,执行完成之后,将结果集放入两个线程共享的内存区,然后向线程A发送消息,让其继续执行,若此时线程B没有其他任务需要执行,便进入阻塞状态。
一、Object.wait()
线程执行执行实例对象的wait()方法之前,必须获取到该对象的锁,执行到实例对象的wait()方法之后,会释放该对象的锁,然后进入阻塞状态。线程A就是扮演的这个角色。示例代码如下:
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
*** 二、obj.notify()***
线程执行实例对象的notify()方法之前,也需要获取到该对象的锁,执行完该对象的notify()方法,并且释放锁之后,会唤醒因为该对象而阻塞的线程之一,具体释放哪个线程就要看线程调度怎么安排了。线程B就是扮演的这个角色。示例代码如下:
synchronized (obj) {
obj.notify();
}
三、趟坑第一步
考虑到我这个项目很可能经常会遇到这种控制线程执行状态的问题,所以我决定创建一个能管理所有线程的类Manager,让它维持一个标志位对象的Map,这样以后遇到类似的问题,就不用再麻烦了。我们创建一个MonitorObject类,用它来产生标志位对象,直接用Object类也完全可以。因为我们不需要什么属性,只要wait()和notify()方法可以用就行。
public class Manager {
public static Map map = new HashMap();
public static void create(String key) {
if (map.get(key) == null) {
Object monitorObject = new Object();
map.put(key, monitorObject);
}
}
public static void doWait(String key) {
create(key);
Object obj = map.get(key);
if (obj == null) return;
synchronized (obj) {
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void doNotify(String key) {
Object obj = map.get(key);
if (obj == null) return;
synchronized (obj) {
obj.notify();
}
}
}
四、线程调用
线程A在调起线程B执行异步任务之后,使用约定好的一个作为标志的key值,调用Manager的doWait()方法,线程B执行完任务之后,调用Manager的doNotify()方法。示例如下:
class ThreadA extends Thread {
public void run() {
/*
唤起线程B执行异步任务
*/
//线程A执行完doWait()方法后进入阻塞状态
Manager.doWait("missonFlag");
/*
线程A唤醒之后的操作
*/
}
}
class ThreadB extends Thread {
public void run() {
/*
执行子任务
*/
Manager.doNotify("missonFlag");
}
}
五、发现问题后进行改造
上面是趟坑的第一步,写这段程序的时候,我刚毕业入职不到两个月,当时还感觉自己棒棒哒,这逻辑看上去完美无瑕。确实,最开始用的时候,确实还算正常,直到有几次发现线程A有时候唤醒不了,一开始还以为是线程B异步任务执行时遇到了网络问题,最后一步步的调试才发现,原来有的时候,doNotify()方法执行在了doWait()方法之前。这样的情况下,线程B执行doNotify()方法的时候,由于线程A还没有被阻塞,所以不会有任何作用。因为notify()方法的作用是在因为该实例对象而阻塞的线程中,唤醒一个线程去执行,线程A并不符合条件。但是线程A还是会顺序执行doWait()方法,这样线程A就被阻塞了,而且也不会受到唤醒它的消息,它就一直死等了,可怜的线程A!于是乎,我把Manager类进行了改造。
现在发现只用Object是不行了,必须添加一个属性才能行,需要添加一个计数器,用来记录等待的状态,执行 obj.wait(),则计数器加1,执行obj.notify()方法,计数器减1。其中还出现过一个小插曲,就是doNotify()方法执行在了doWait()方法之前的时候,MonitorObject的实例也没有被创建,所以把create()方法也单独拿了出来,在线程A唤起异步任务之前调用。
class MonitorObject {
public int count = 0;
}
public class Manager {
public static Map map = new HashMap();
public static void create(String key) {
if (map.get(key) == null) {
MonitorObject monitorObject = new MonitorObject();
map.put(key, monitorObject);
}
}
public static void doWait(String key) {
MonitorObject obj = map.get(key);
if (obj == null) return;
synchronized (obj) {
try {
if (obj.count < 0){
obj.count++;
return;
}
obj.count++;
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void doNotify(String key) {
MonitorObject obj = map.get(key);
if (obj == null) return;
synchronized (obj) {
obj.count--;
obj.notify();
}
}
}
六、改造之后使用Manager
改造之后,线程A在使用Manager之前,必须先创建MonitorObject对象,这一步是为了保证不管doWait()和doNotify()哪个先执行,计数器的作用能正常发挥。改造之后的Manager仍然不能控制doWait()和doNotify()哪个先执行,但是如果doNotify()先于doWait()执行,在执行doWait()操作时,线程不会阻塞,只会把MonitorObject的计数器做自增操作。因为doNotify()先执行,说明异步任务已经完成,线程A已经无需等待了。
示例代码如下:
class ThreadA extends Thread {
public void run() {
Manager.create("missonFlag");
/*
唤起线程B执行异步任务
*/
//线程A执行完doWait()方法后进入阻塞状态
Manager.doWait("missonFlag");
/*
线程A唤醒之后的操作
*/
}
}
class ThreadB extends Thread {
public void run() {
/*
执行子任务
*/
Manager.doNotify("missonFlag");
}
}