最近在工作时,有一位同事遇到了一个查询优化的问题,过来向我请教。
问题大致是这样的:要给前端提供一个查询的接口,而在此接口中要去调用远端的10几个ESB接口,由于要调用的远端ESB接口数量过多导致此接口响应异常缓慢,甚至于达到了十几秒钟。现在要想办法去优化这个接口。这十几个远端的ESB接口不用关心调用顺序,不需要互相依赖。
针对于这个问题,我其实第一反应想到的是采用线程池,实现多线程调用,但同事告诉我说组长要求,先不要考虑使用线程池,因为线程池涉及到线程回收、维护等各种情况可能会在长时间运行后出现不可控的结果。
第二反应我想到的是采用非阻塞的方式掉用这10几个远端的ESB接口,但是了解后发现非阻塞的在java中主要支持的是NIO和socket通讯,所以这种方式也不行。
最后我想到的是,既然首选不能是线程池,那么能不能简单粗暴的采用多线程的模式?这样线程执行完毕后会自我销毁,就不用考虑回收维护等这些负责的情况了。开启多个线程让不同的线程去调用不同的远端ESB接口,在所有的线程都执行完毕之后再进行最终查询结果的整合。答案是可行的,直接采用Java自带的CountDownLatch + 多线程 去实现。具体实现过程大致如下(只是一个模拟过程,公司代码有规定不能外泄):
public class ThreadTest {
//模拟不同的线程 返回不同的处理结果
private Map map1 = new HashMap();
private Map map2 = new HashMap();
private Map map3 = new HashMap();
/**
* 主线程
*/
@Test
public void test() {
//开启线程个数
int threadCount = 3;
//所有线程阻塞,然后统一开始
CountDownLatch begin = new CountDownLatch(1);
//主线程阻塞,直到所有分线程执行完毕
CountDownLatch end = new CountDownLatch(threadCount);
//开始多线程
begin.countDown();
for (Integer i = 0; i < threadCount; i++) {
if (i == 0){
Runnable runnable = dealSomeThing1(i, end,map1);
new Thread(runnable).start();
}
if (i == 1){
Runnable runnable = dealSomeThing2(i, end,map2);
new Thread(runnable).start();
}
if (i == 2){
Runnable runnable = dealSomeThing3(i, end,map3);
new Thread(runnable).start();
}
}
//多个线程都执行结束
try {
end.await(); //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
System.out.println("多个线程都执行结束,可以做自己的事情了");
String result1 = (String) map1.get("test");
System.out.println(result1);
String result2 = (String) map2.get("test");
System.out.println(result2);
String result3 = (String) map3.get("test");
System.out.println(result3);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("多线程执行中出错了,凉凉了!!!");
}
}
/**
* 工作线程
* 本方法 是在构造多线程要做的事情
* <p>
* =====================可以做的事===================
* 当然可以传入ConcurrentHashMap之类的线程安全的 类
* 来记录线程中的处理结果之类的
* 最后 在多线程都执行完了以后 就可以对处理结果进行操作了
* ==================================================
*
* @param threadNum 当前线程编号
* @param end
* @return
*/
private Runnable dealSomeThing1(int threadNum, CountDownLatch end,Map map) {
Runnable runnable = new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("线程" + threadNum + ":--------------------->开始工作");
Thread.sleep(3000);//模拟调用目标ESB接口
map.put("test","testResult1");
end.countDown(); //执行完成后将count值减1
}
};
return runnable;
}
private Runnable dealSomeThing2(int threadNum, CountDownLatch end,Map map2) {
Runnable runnable = new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("线程" + threadNum + ":--------------------->开始工作");
Thread.sleep(3000);//模拟调用目标ESB接口
map2.put("test","testResult2");
end.countDown(); //执行完成后将count值减1
}
};
return runnable;
}
private Runnable dealSomeThing3(int threadNum, CountDownLatch end,Map map3) {
Runnable runnable = new Runnable() {
@SneakyThrows
@Override
public void run() {
System.out.println("线程" + threadNum + ":--------------------->开始工作");
Thread.sleep(3000);//模拟调用目标ESB接口
map3.put("test","testResult3");
end.countDown(); //执行完成后将count值减1
}
};
return runnable;
}
}
对CountDownLatch类的简单介绍如下:
1、CountDownLatch是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。
2、存在于java.util.concurrent并发包下(所以线程也是安全的)。
3、CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
4、CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
5、CountDownLatch类有三个主要的方法:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
对于以上模拟的代码,开启了三个线程分别去调用了三个方法dealSomeThing1、dealSomeThing2、dealSomeThing3, 在每个方法执行完毕之后采用end.countDown()方法使count值减1,end.await()方控制当count值为0时(同时也表示所有的子线程已经执行完毕)开始对每个多线程的执行结果进行整合。按照往常的方式调用耗时至少是9秒钟。而按照这种方式耗时大约只有3到4秒钟,时间节约了至少一半。成功解决了此问题。
运行结果如下: