看起来有点绕,说一个具体的场景应该容易理解一点:
- 从APP向服务器发送一个改变灯光的HTTP请求,服务器返回执行是否成功的结果;
- 服务器接收到HTTP请求后,通过MQTT向台灯下发控制指令,等待台灯回复①,返回结果;
- 台灯接收到MQTT指令后,执行命令,然后通过MQTT回复消息给服务器②;
- 通常来说,后台会有一个专门的服务订阅一个固定的Topic,接收台灯的消息,所以①中,不可能是处理HTTP请求的线程订阅Topic,那么订阅Topic的服务,是怎么把消息通知到HTTP线程的呢?
最开始我的做法是,订阅Topic的服务,将消息解析后放入redis,然后HTTP线程在下发消息后,通过redis的超时读机制,阻塞线程,获取处理结果,如果超时未返回则认为不成功。
该方法的弊端在于……就为了这个要求,强制绑定redis,带价太大,而且实现也不是很好。
在Golang中有协程和channel的概念,协程之间可以通过channel去通信,只要设计合理,就可以很容易的实现该功能,但是在JAVA中,没有channel,替代品就是Object.wait()和Object.notify()线程等待/通知机制,直接上代码!
package com.shrimp.dome;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
private static ConcurrentHashMap<String, Object> msgManager = new ConcurrentHashMap<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i < 10; i++) {
cachedThreadPool.submit(new WaitThread(i));
}
Thread.sleep(5000);
new Thread(new Connsumer()).start();
}
static class Connsumer implements Runnable {
@Override
public void run() {
msgManager.forEach((key, value) -> {
System.out.println("dell with : " + ((Event) value).messageId);
synchronized (value) {
value.notify();
}
});
}
}
static class WaitThread implements Runnable {
private int count;
WaitThread(int count) {
this.count = count;
}
@Override
public void run() {
Event event = new Event();
event.messageId = count + "";
synchronized (event) {
msgManager.put(event.messageId, event);
try {
long timeout = 1000 * 10;
long begin = System.currentTimeMillis();
event.wait(timeout);
long end = System.currentTimeMillis();
if (end - begin < timeout) {
System.out.println("continue by notify");
} else {
System.out.println("continue after timeout");
}
msgManager.remove(event.messageId);
System.out.println("WaitThread cont continue : " + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Event {
String messageId;
}
}
实现思路很简单,首先需要有一个全局的Map,以便两个线程都能访问到,一般通过消息ID作为键,利于检索;
HTTP线程在发送设备控制请求后,创建事件对象event,通过synchronized获取对象锁,然后放入上面的全局Map中,调用event.wait(timeout),这里最好是wait(timeout)而不是wait(),否则改线程会一直阻塞占资源。等待其他线程notify后,在map删除对应的event释放内存,就可以回复response了;
对于订阅Topic的服务,就可以通过消息带的消息ID,取得对应的event,然后调用event.notify()方法,就可以通知到HTTP线程继续执行。
通过上面的方法,就完成了同步回复异步消息的功能了。
不知道这样的方法对不对,但是测试下来是没问题的,可能在效率和实现方面有点抓急。如果有更好的方法,欢迎留言告诉我,谢谢!