关于这一块一前一直是一个盲点,看了看源码总结一下把。这一块内容和线程池也是息息相关的
线程池的顶级接口是Executor接口,里面只有一个未实现方法是
void execute(Runnable command);
下来是ExecutorService接口,继承自Executor接口,里面多 了很多方法,比较重要的几个方法是
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
提交多个任务,并返回与每个任务对应的Futue。也就是说,任务彼此之间不会相互影响,可以通过future跟踪每一个任务的执行情况,比如是否被取消,是正常完成,还是异常完成。调用该方法的线程会阻塞,直到tasks全部执行完成(正常完成/异常退出)
如果线程在等待invokeAll执行的过程中被中断,那么线程池就会终止所以正在被执行的任务,并抛出异常。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
给定的超时期满,还没有完成的任务会被取消,即Future.isCancelled()返回true;在超时期之前,无论是正常完成还是异常终止的任务,Future.isCancelled()返回false。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
提交多个任务,一旦有一个任务完成,就会终止其他任务的执行,如果没有一个任务完成,那么就会抛出异常。
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
如果在超时之前,所有任务已经都是异常终止,那就没有必要在等下去了;如果超时之后,仍然有正在运行或等待运行的任务,那么会抛出TimeoutException。
void shutdown();
终止当前线程池,但是执行中的任务会执行到结束,等待的任务不被执行
List<Runnable> shutdownNow();
终止当前线程池,执行中的任务立即结束,等待的任务不被执行
boolean isShutdown();
如果调用了上面的两个shutdown方法,就返回true
boolean isTerminated();
如果在shutdown之后,所以任务也都结束了,线程池处于终结状态,那么返回true
<T> Future<T> submit(Runnable task, T result);
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
上面的三个方法都是在线程池中提交一个任务,包括callable类型或者是runable类型。返回future对象
AbstractExecutorService抽象类实现了ExecutorService,对上面的submit方法也有了实现,其最终还是在调用顶级接口Executor中的execute方法,但是AbstractExecutorService并没有实现execute方法,该方法在他的实现类ThreadPoolExecutor中实现。
从上面可以看到submit的参数类型有两种分别是runable和callable,但是在AbstractExecutorService中对submit实现上,都是将两种对象转化成了FutureTask对象,然后将这个转化之后的对象传入execute方法中。
为啥runnable对象和callable对象可以转化成futureTask对象呢?下面从Future接口说起。这个接口的方法如下:
-
cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
-
isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
-
isDone方法表示任务是否已经完成,若任务完成,则返回true;
-
get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
-
get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
之后RunnableFuture接口继承了Runnable接口和Future接口,重点,继承了两个接口,然后FutureTask类是RunnableFuture的实现类。
FutureTask的构造方法有两个,一个接受Callable对象,另一个接受runnable对象,但是接受了runnable对象之后会调用Executors.callable()方法,将这个runnable对象转化成callable对象,具体的转化就是通过一个RunnableAdapter类生成一个callabled对象,然后这个callable对象的call方法就是runnable的run方法。那就不用想也明白了,FutureTask中的实现的run方法一定是执行的直接传进来的callable对象或者转化来的call able对象的call方法。事实也是这样。
那这个callable到底是什么,怎么这么牛逼啊,runnable传进来还要转化成他。
public interface Callable<V> {
V call() throws Exception;
}
因为call方法执行完可以返回参数,就这么简单,而run的返回是void。ok,正点来了,当我们起一个线程去做一个任务的时候调用run方法是没有返回值的,如果我们需要的话就只能用callable了。因为call方法是可以返回结果的。
回头再看submit方法,传入一个runnable或者callable,开启一个新的线程去执行,然后返回一个future对象,用来对任务进行操作,是不是很牛逼的设计。
这里上一个例子看看,这个例子就是用线程池加上future来实现多线程的一个方式。
public class FutureDemo {
//创建一个容量为1的线程池
static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) throws Exception {
//创建线程并提交线程,同时获取一个future对象
Thread subThread = new Thread(new SubThread());
Future future = executorService.submit(subThread);
//主线程处理其他工作,让子线程异步去执行
mainWork();
//阻塞,等待子线程结束
future.get();
System.out.println("Now all thread done!");
//关闭线程池
executorService.shutdown();
}
//主线程工作
private static void mainWork(){
System.out.println("Main thread start work!");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Main Thread work done!");
}
/**
* 子线程类
* @author fuhg
*/
private static class SubThread implements Runnable{
public void run() {
// TODO Auto-generated method stub
System.out.println("Sub thread is starting!");
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Sub thread is stopping!");
}
}
}
当然如果要用future实现多线程并不是一定要用线程池,只是ExecutorService中的submit可以直接返回future对象,如果我们自己定义future对象的话,就可以不用线程池来实现。
Callable<Chuju> onlineShopping = new Callable<Chuju>() { @Override public Chuju call() throws Exception { System.out.println("第一步:下单"); System.out.println("第一步:等待送货"); Thread.sleep(5000); // 模拟送货时间 System.out.println("第一步:快递送到"); return new Chuju(); } }; FutureTask<Chuju> task = new FutureTask<Chuju>(onlineShopping); new Thread(task).start(); 这里我们自己定义了futuretask对象,然后直接作为参数传递给thread并运行也是ok的,对任务的操作直接用我们自己定义的task就行了。
Callable<String> callable=new Callable<String>() {
@Override
public String call() throws Exception {
// TODO Auto-generated method stub
System.out.println("start call");
Thread.sleep(5000);
System.out.println("end call");
return "hello word";
}
};
ExecutorService executorService = Executors.newFixedThreadPool(1);
//创建线程并提交线程,同时获取一个future对象
Future future = executorService.submit(callable);
//主线程处理其他工作,让子线程异步去执行
//mainWork();
//阻塞,等待子线程结束
try {
System.out.println(future.get());//获取子线程执行结束之后返回的结果
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("Now all thread done!");//如果调用了get但是子线程还没有执行结束,那么主线程就会阻塞,那么这句话要在子线程
//执行结束之后,主线程开始执行才能输出。
futuretask有一个重要的属性就是state
private volatile int state; // 注意volatile关键字
/**
* 在构建FutureTask时设置,同时也表示内部成员callable已成功赋值,
* 一直到worker thread完成FutureTask中的run();
*/
private static final int NEW = 0;
/**
* woker thread在处理task时设定的中间状态,处于该状态时,
* 说明worker thread正准备设置result.
*/
private static final int COMPLETING = 1;
/**
* 当设置result结果完成后,FutureTask处于该状态,代表过程结果,
* 该状态为最终状态final state,(正确完成的最终状态)
*/
private static final int NORMAL = 2;
/**
* 同上,只不过task执行过程出现异常,此时结果设值为exception,
* 也是final state
*/
private static final int EXCEPTIONAL = 3;
/**
* final state, 表明task被cancel(task还没有执行就被cancel的状态).
*/
private static final int CANCELLED = 4;
/**
* 中间状态,task运行过程中被interrupt时,设置的中间状态
*/
private static final int INTERRUPTING = 5;
/**
* final state, 中断完毕的最终状态,几种情况,下面具体分析
*/
private static final int INTERRUPTED = 6;
然后还给出了四种可能的结果
Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
futuretask的cancel方法,get方法都会应用到这些状态。其中get方法会对调用线程进行阻塞。
先聊聊线程池的提交方式
谈到 Future 的时候,我们基本上就会想到线程池,想到它的几种提交方式。
先是最简单的,execute 方式提交,不关心返回值的,直接往线程池里面扔任务就完事:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
//execute(Runnable command)方法。没有返回值
executor.execute(() -> {
System.out.println("关注why技术");
});
Thread.currentThread().join();
}
}
可以看一下 execute 方法,接受一个 Runnable 方法,返回类型是 void:
然后是 submit 方法。你知道线程池有几种 submit 方法吗?
虽然你经常用,但是可能你从来没有关心过人家。呸,渣男:
有三种 submit。这三种按照提交任务的类型来算分为两个类型。
-
提交执行 Runnable 类型的任务。
-
提交执行 Callable 类型的任务。
但是返回值都是 Future,这才是我们关心的东西。
也许你知道线程池有三种 submit 方法,但是也许你根本不知道里面的任务分为两种类型,你就只知道往线程池里面扔,也不管扔的是什么类型的任务。
我们先看一下 Callable 类型的任务是怎么执行的:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
Future<String> future = executor.submit(() -> {
System.out.println("关注why技术");
return "这次一定!";
});
System.out.println("future的内容:" + future.get());
Thread.currentThread().join();
}
}
这里利用 lambda 表达式,直接在任务体里面带上一个返回值,这时你看调用的方法就变成了这个:
运行结果也能拿到任务体里面的返回了。输出结果如下:
好,接下来再说说 submit 的任务为 Runable 类型的情况。
这个时候有两个重载的形式:
标号为 ① 的方法扔进去一个 Runable 的任务,返回一个 Future,而这个返回的 Future ,相当于是返回了一个寂寞。下面我会说到原因。
标号为 ② 的方法扔进去一个 Runable 的任务的同时,再扔进去一个泛型 T ,而巧好返回的 Future 里面的泛型也是 T,那么我们大胆的猜测一下这就是同一个对象。如果是同一个对象,说明我们可以一个对象传到任务体里面去一顿操作,然后通过 Future 再次拿到这个对象的。一会就去验证。
来,先验证标号为 ① 的方法,我为啥说它返回了一个寂寞。
首先,还是先把测试案例放在这里:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
Future<?> future = executor.submit(() -> {
System.out.println("关注why技术");
});
System.out.println("future的内容:" + future.get());
Thread.currentThread().join();
}
}
可以看到,确实是调用的标号为 ① 的方法:
同时,我们也可以看到 future.get() 方法的返回值为 null。
你说,这不是返回了一个寂寞是干啥?
当你想用标号为 ① 的方法时,我劝你直接用 execute 方式提交任务。还不需要构建一个寂寞的返回值,徒增无用对象。
接下来,我们看看标号为 ② 的方法是怎么用的:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
AtomicInteger atomicInteger = new AtomicInteger();
Future<AtomicInteger> future = executor.submit(() -> {
System.out.println("关注why技术");
//在这里进行计算逻辑
atomicInteger.set(5201314);
}, atomicInteger);
System.out.println("future的内容:" + future.get());
Thread.currentThread().join();
}
}
可以看到改造之后,确实是调用了标号为 ② 的方法:
future.get() 方法的输出值也是异步任务中我们经过计算后得出的 5201314。
你看,渣男就是这样,明明不懂你,还非得用甜言蜜语来轰炸你。呸。
好了。综上,线程池的提交方式一共有四种:一种 execute,无返回值。三种 submit,有返回值。
submit 中按照提交任务的类型又分为两种:一个是 Callable,一个是 Runable。
submit 中 Runable 的任务类型又有两个重载方法:一个返回了个寂寞,一个返回了个渣男。哦,不。一个返回了个寂寞,一个返回了个对象。
这个时候就有人要站出来说:你说的不对,你就是瞎说,明明就只有 execute 这一种提交方式。
是的,“只有 execute 这一种提交方式”这一种说法也是没错的。
请看源码:
三种 submit 方法里面调用的都是 execute 方法。
能把前面这些方法娓娓道来,从表面谈到内在的这种人,才是好人。
只有爱你,才会把你研究透。
当然,还有这几种提交方式,用的不多,就不展开说了:
写到这里我不禁想起了我的第三篇文章,真是奇怪的时间线开始收缩了的感觉,《有的线程它死了,于是它变成一道面试题》,这篇文章里面聊到了不同提交方式,对于异常的不同处理方式。
我就问你:一个线程池中的线程异常了,那么线程池会怎么处理这个线程?
你要是不知道,可以去看看这篇文章,毕竟,有可能在面试的时候遇到的:
好,上面这些东西捋清楚了之后。我们再聚焦到返回值 Future 上:
从上面的代码我们可以看出,当我们想要返回值的时候,都需要调用下面的这个 get() 方法:
而从这个方法的描述可以看出,这是一个阻塞方法。拿不到值就在那里等着。当然,还有一个带超时时间的 get 方法,等指定时间后就不等了。
呸,渣男。没耐心,这点时间都舍不得等。
总之就是有可能要等的。只要等,那么就是阻塞。只要是阻塞,就是一个假异步。
所以总结一下这种场景下返回的 Future 的不足之处:
-
只有主动调用 get 方法去获取值,但是有可能值还没准备好,就阻塞等待。
-
任务处理过程中出现异常会把异常隐藏,封装到 Future 里面去,只有调用 get 方法的时候才知道异常了。
写到这里的时候我不禁想起一个形象的例子,我给你举一个。
假设你想约你的女神一起去吃饭。女神嘛,肯定是要先画个美美的妆才会出去逛街的。而女神化妆就可以类比为我们提交的一个异步任务。
假设你是一个小屌丝,那么女神就会对你说:我已经开始化妆了,你到楼下了就给我打电话。
然后你就收拾行头准备出发,这就是你提交异步任务后还可以做一些自己的事情。
你花了一小时到了女神楼下,打电话给她:女神你好,我到你楼下了。
女神说:你先等着吧,我的妆还没画好呢。
于是你开始等待,无尽的等待。这就是不带超时时间的 future.get() 方法。
也有可能你硬气一点,对女神说:我最多再等 24 小时哈,超过 24 小时不下楼,我就走了。
这就是带超时时间的 future.get(timeout,unit) 方法:
结果 24 小时之后,女神还没下来,你就走了。
当然,还有一种情况就是你到楼下给女神打电话,女神说:哎,今天我男神约我出去看电影,就不和你去吃饭了哈。本来我想提前给你说的,但是我又记不起你电话,只有你打过来我才能告诉你。就这样,你自己玩去吧。
这就相当于异步任务执行过程中抛出了异常,而你只有在调用了 get 方法(打电话操作)之后才知道原来异常了。
而真正的异步是你不用等我,我好了我就叫你。
就像女神接到男神的电话时说的:我需要一点时间准备一下,你先玩自己的吧,我一会好了给你打电话。
这让我想起了好莱坞原则:Don't Call Us,We'll Call you!
接下来,让我们见识一下真正的异步。
什么叫真正的:“你先玩自己的,我一会好了叫你。”
Guava 的 Future
女神说的:“好了叫你”。
就是一种回调机制。说到回调,那么我们就需要在异步任务提交之后,注册一个回调函数就行。
Google 提供的 Guava 包里面对 JDK 的 Future 进行了扩展:
新增了一个 addListenter 方法,入参是一个 Runnable 的任务类型和一个线程池。
使用方法,先看代码:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFuture<String> listenableFuture = executor.submit(() -> {
System.out.println(Thread.currentThread().getName()+"-女神:我开始化妆了,好了我叫你。");
TimeUnit.SECONDS.sleep(5);
return "化妆完毕了。";
});
listenableFuture.addListener(() -> {
try {
System.out.println(Thread.currentThread().getName()+"-future的内容:" + listenableFuture.get());
} catch (Exception e) {
e.printStackTrace();
}
}, executor);
System.out.println(Thread.currentThread().getName()+"-等女神化妆的时候可以干点自己的事情。");
Thread.currentThread().join();
}
}
首先创建线程池的方式变了,需要用 Guava 里面的 MoreExecutors 方法装饰一下:
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
然后用装饰后的 executor 调用 submit 方法(任意一种),就会返回 ListenableFuture ,拿到这个 ListenableFuture 之后,我们就可以在上面注册监听:
所以,上面的程序我们调用的是入参为 callable 类型的接口:
从运行结果可以看出来:获取运行结果是在另外的线程里面执行的,完全没有阻塞主线程。
和之前的“假异步”还是有很大区别的。
除了上面的 addListener 方法外,其实我更喜欢用 FutureCallback 的方式。
可以看一下代码,非常的直观:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
ListeningExecutorService executor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFuture<String> listenableFuture = executor.submit(() -> {
System.out.println(Thread.currentThread().getName()+"-女神:我开始化妆了,好了我叫你。");
TimeUnit.SECONDS.sleep(5);
return "化妆完毕了。";
});
Futures.addCallback(listenableFuture, new FutureCallback<String>() {
@Override
public void onSuccess(@Nullable String result) {
System.out.println(Thread.currentThread().getName()+"-future的内容:" + result);
}
@Override
public void onFailure(Throwable t) {
System.out.println(Thread.currentThread().getName()+"-女神放你鸽子了。");
t.printStackTrace();
}
});
System.out.println(Thread.currentThread().getName()+"-等女神化妆的时候可以干点自己的事情。");
Thread.currentThread().join();
}
}
有 onSuccess 方法和 onFailure 方法。
上面的程序输出结果为:
如果异步任务执行的时候抛出了异常,比如女神被她的男神约走了,异步任务改成这样:
ListenableFuture<String> listenableFuture = executor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
TimeUnit.SECONDS.sleep(5);
throw new Exception("男神约我看电影,就不和你吃饭了。");
});
最终的运行结果就是这样:
是的,女神去看电影了。她一定只是不想吃饭而已。
加强版的Future - CompletableFuture
第一小节讲的 Future 是 JDK 1.5 时代的产物:
经过了这么多年的发展,Doug Lea 在 JDK 1.8 里面引入了新的 CompletableFuture :
到了 JDK 1.8 时代,这才是真正的异步编程。
CompletableFuture 实现了两个接口,一个是我们熟悉的 Future ,一个是 CompletionStage。
CompletionStage接口,你看这个接口的名称中有一个 Stage :
可以把这个接口理解为一个任务的某个阶段。所以多个 CompletionStage 链接在一起就是一个任务链。前一个任务完成后,下一个任务就会自动触发。
CompletableFuture 里面的方法非常的多。
由于篇幅原因,我就只演示一个方法:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "化妆完毕了。";
});
completableFuture.whenComplete((returnStr, exception) -> {
if (exception == null) {
System.out.println(Thread.currentThread().getName() + returnStr);
} else {
System.out.println(Thread.currentThread().getName() + "女神放你鸽子了。");
exception.printStackTrace();
}
});
System.out.println(Thread.currentThread().getName() + "-等女神化妆的时候可以干点自己的事情。");
Thread.currentThread().join();
}
}
该方法的执行结果如下:
我们执行的时候并没有指定用什么线程池,但是从结果可以看到也是异步的执行。
从输出日志中是可以看出端倪的,ForkJoinPool.commonPool() 是其默认使用的线程池。
当然,我们也可以自己指定。
这个方法在很多开源框架里面使用的还是非常的多的。
接下来主要看看 CompletableFuture 对于异常的处理。我觉得非常的优雅。
不需要 try-catch 代码块包裹,也不需要调用 Future.get() 才知道异常了,它提供了一个 handle 方法,可以处理上游异步任务中出现的异常:
public class JDKThreadPoolExecutorTest {
public static void main(String[] args) throws Exception {
CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + "-女神:我开始化妆了,好了我叫你。");
throw new RuntimeException("男神约我看电影了,我们下次再约吧,你是个好人。");
}).handleAsync((result, exception) -> {
if (exception != null) {
System.out.println(Thread.currentThread().getName() + "-女神放你鸽子了!");
return exception.getCause();
} else {
return result;
}
}).thenApplyAsync((returnStr) -> {
System.out.println(Thread.currentThread().getName() + "-" + returnStr);
return returnStr;
});
System.out.println(Thread.currentThread().getName() + "-等女神化妆的时候可以干点自己的事情。");
Thread.currentThread().join();
}
}
由于女神在化妆的时候,接到男神的电话约她看电影,就只能放你鸽子了。
所以,上面程序的输出结果如下:
如果,你顺利把女神约出来了,是这样的:
好了,女神都约出来了,文章就到这里了。去干正事吧
一个应用场景
业务场景
我们公司做的是加油业务,用户可以在app上通过当前位置和目标位置来查询地图路线以及路线途径的所有加油站,路线查询会调用高德地图提供的接口,途径油站则根据返回的路线信息进行查询,所以当用户输入起始位置和目标位置点击查询后会做以下几步操作:
- 调用高德地图接口获取路线
- 根据高德地图返回的路线信息去查询途径的所有油站
问题点
公司发展很快,入驻平台的油站很多,当用户输入的起始地和目标地距离很远时,那么途径油站的数量会很大,单独采取普通查询库的方式会很耗时,并且途径油站的数据必须是实时的,所以无法使用缓存来提高接口响应效率。第一步是调用高德地图API,也会存在一定的延迟。那么我们应该怎么优化呢?
为了降低接口耗时,提高用户体验,我们需要对接口实现进行优化,调用高德API我们无法优化,所以只能优化查询途径油站这部分。
优化思路
当油站过多时,一次查询会很耗时,所以我们可以考虑分批多线程并发的去查询,将一段很长的路线按照路径长度分成若干个条件,比如一段路径长达800km,我们可以将这800km的查询参数拆分成若干个距离较小的参数集合(ps:举例方便大家理解,实际路径规划查询都是根据经纬度、距离等多重参数进行查询的)。比如,{[0,50],[50,100],[100,150].....[750,800]},这时我们开启多个线程去并发的根据新的查询条件去查询,最后将结果拼接封装返回,从而达到降低查询时间的目的。
虽然思路很容易理解,但是实现起来有两个需要注意的地方,我列出来看看大家有没有考虑到。
- 根据业务场景,这里不是单纯的异步查询就可以的,而是需要所有的线程都执行完后并且组合查询结果后进行返回,所以这里需要进行同步控制。这里我们使用jdk提供的CountDownLatch同步组件实现。
- 线程内操作需要有返回值,使用Callable接口以及FutureTask搭配实现。
具体实现
1.通常来说,我们定义线程需要实现Runnable接口,但是对于需要返回值的线程,就需要线程实现Callable接口了。
@Component
@Slf4j
@Scope("protoType") // 这里需要注意Spring默认注入的Bean都是单例的,当前业务场景下肯定需要多个线程去执行查询操作,所以这里声明组件为protoType模式
public class PathPlanTask implements Callable<List<Object>> {
// 查询参数
private PathPlanPartQuery pathPlanPartQuery;
private CountDownLatch countDownLatch;
@Override
public List<Object> call() throws Exception {
try {
//TODO 业务查询
List<Object> result = queryList(pathPlanPartQuery);
// 返回结果
return result;
}catch (Exception e){
// 错误日志打印
log.error("query PathByGasstation error!");
}finally{
// 类似 i-- 的操作,当减到0的时候,countDownLatch.await()就会放行,否则会一直阻塞。
countDownLatch.countDown();
}
}
public void setPathPlanPartQuery(PathPlanPartQuery pathPlanPartQuery){
this.pathPlanPartQuery = pathPlanPartQuery;
}
public void setCountDownLatch(CountDownLatch countDownLatch){
this.countDownLatch = countDownLatch;
}
private List<Object> queryList(PathPlanPartQuery pathPlanPartQuery) {
// TODO 具体查询逻辑,这里省略
return Lists.newArrayList();
}
}
复制代码
2.Callable通常和FutureTask搭配使用,通过FutureTask的get方法获取到线程的返回值。
// 通常定义为工具类进行获取
private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(8, 20, 1000,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(50), new ThreadPoolExecutor.AbortPolicy());
// 业务代码
private List<Object> queryGasInfoBaseDtoList(List<PathPlanQueryParam> queryParamList) {
long stMills = System.currentTimeMillis();
// 定义线程池来进行多线程的管理,通过Util获取静态的线程池
// 定义countDownLatch,构造函数传递参数集合的size,该集合具体参数可以参考
// 上面举的例子,{[0,50],[50,100],[100,150]...[750,800]}
CountDownLatch countDownLatch = new CountDownLatch(queryParamList.size());
// 批量查询,定义FutureTask集合
List<FutureTask<List<GasInfoBaseResponseDto>>> futureTaskList = Lists.newArrayList();
try {
// 遍历查询参数集合
for (PathPlanQueryParam queryParam : queryParamList) {
// 这里使用getBean方式获取。
PathPlanTask pathPlanTask =
ApplicationContextProvider.getBean("pathPlanTask", PathPlanTask.class);
// 设置countDownLatch
pathPlanTask.setCountDown(countDownLatch);
// 获取查询参数
PathPlanPartQuery pathPlanPartQuery = getPathPlanPartQuery(queryParam);
pathPlanTask.setPathPlanPartQuery(pathPlanPartQuery);
// 定义FutureTask,将定义好的Callable实现类作为构造参数
FutureTask<List<GasInfoBaseResponseDto>> futureTask = new FutureTask<>(pathPlanTask);
// 交给线程池去执行
poolExecutor.submit(futureTask);
// 添加futureTask集合
futureTaskList.add(futureTask);
}
// 这里会一直进行阻塞,直到countDownLatch.countDown()方法将创建时传递的size参数减为0后放行。
// 这块可以保证多个线程全部执行完后进行最终返回。
countDownLatch.await();
// 多个线程执行完后我们拼接最终结果
List<Object> gasInfoDtoList = Lists.newArrayList();
for (FutureTask<List<Object>> futureTask : futureTaskList) {
// 通过futrueTask的get方法获取返回值,当线程还在执行未返回时执行futureTask.get()会被阻塞
List<Object> baseResponseDtoList = futureTask.get();
if (CollectionUtils.isNotEmpty(baseResponseDtoList)) {
gasInfoDtoList.addAll(baseResponseDtoList);
}
}
return gasInfoDtoList;
} catch (Exception e) {
log.error("queryGasInfoBaseDtoList_err", e);
} finally {
log.info("queryGasInfoBaseDtoList_requestId:{},batchTimes:{},cost:{}", pointRequestOld.getRequestId(),
pointRequestOld.getBatchTimes(), System.currentTimeMillis() - stMills);
}
return null;
}
复制代码
总结
以上是我在工作中遇到的多线程实际应用的场景,总结下来就是通过线程池开启多个Callable线程去分批查询数据, 引入CountDownLatch组件来控制查询结束时机,而后利用FutureTask的get方法获取最终结果拼装返回。
作者:森屿说
链接:https://juejin.cn/post/6903544822221504526
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。