实现异步的方式
- 多线程
- 线程
- 线程池
- 推荐使用的线程池
- 线程池线程启动方式
- Spring 的异步方法去执行(无返回值)
- Spring的异步方法 + Future接收返回值
- 原生Future方法
多线程
线程
- 实现线程的方式
- 继承Thread
无返回值 - 实现Runnable
该方式无返回值,当通过ExecutorService.submit() 方式启动时也可返回结果值 - 实现Callable
有返回值
注意点
- start() 和 run() 启动的区别
用start()来启动线程,实现了真正意义上的启动线程,此时会出现异步执行的效果,即在线程的创建和启动中所述的随机性。‘
而如果使用run()来启动线程,就不是异步执行了,而是同步执行,不会达到使用线程的意义。 - ?
线程池
推荐使用的线程池
有基础的四种线程池但是阿里巴巴开发规范不建议使用这四种线程池
分别为
- newSingleThreadExecutor
- newCachedThreadPool
- newFixedThreadPool
- newScheduleThreadPool
而是推荐使用 ThreadPoolExecutor 的方式来创建线程池,它有四种构造方法.
主要熟悉它的7个参数
public ThreadPoolExecutor(int corePoolSize, // 1
int maximumPoolSize, // 2
long keepAliveTime, // 3
TimeUnit unit, // 4
BlockingQueue<Runnable> workQueue, // 5
ThreadFactory threadFactory, // 6
RejectedExecutionHandler handler ) { //7
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
名称 | 类型 | 含义 |
corePoolSize | int | 核心线程池大小 |
maximumPoolSize | int | 最大线程池大小 |
keepAliveTime | long | 线程最大空闲时间 |
unit | TimeUnit | 时间单位 |
workQueue | BlockingQueue | 线程等待队列 |
threadFactory | ThreadFactory | 线程创建工厂 |
handler | RejectedExecutionHandler | 拒绝策略 |
参数理解:
- 当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建一个固定大小的线程池。 通过将maximumPoolSize设置为基本上无界的值,例如Integer.MAX_VALUE,您可以允许池容纳任意数量的并发任务。 通常,核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize和setMaximumPoolSize进行动态更改。
- Keep-alive times 线程存活时间
如果线程池当前拥有超过corePoolSize的线程,那么多余的线程在空闲时间超过keepAliveTime时会被终止 ( 请参阅getKeepAliveTime(TimeUnit) )。这提供了一种在不积极使用线程池时减少资源消耗的方法。
如果池在以后变得更加活跃,则应构建新线程。 也可以使用方法setKeepAliveTime(long,TimeUnit)进行动态调整。
防止空闲线程在关闭之前终止,可以使用如下方法:
setKeepAliveTime(Long.MAX_VALUE,TimeUnit.NANOSECONDS);
默认情况下,keep-alive策略仅适用于存在超过corePoolSize线程的情况。 但是,只要keepAliveTime值不为零,方法allowCoreThreadTimeOut(boolean)也可用于将此超时策略应用于核心线程。 - Queuing 队列
BlockingQueu用于存放提交的任务,队列的实际容量与线程池大小相关联。
- 如果当前线程池任务线程数量小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中取一个空闲线程。
- 如果当前线程池任务线程数量大于核心线程池数量,执行器总是优先从线程队列中取一个空闲线程,而不是创建一个任务线程。
- 如果当前线程池任务线程数量大于核心线程池数量,且队列中无空闲任务线程,将会创建一个任务线程,直到超出maximumPoolSize,如果超时maximumPoolSize,则任务将会被拒绝。
主要有三种队列策略: - Direct handoffs 直接握手队列
Direct handoffs 的一个很好的默认选择是 SynchronousQueue,它将任务交给线程而不需要保留。这里,如果没有线程立即可用来运行它,那么排队任务的尝试将失败,因此将构建新的线程。
此策略在处理可能具有内部依赖关系的请求集时避免锁定。Direct handoffs 通常需要无限制的maximumPoolSizes来避免拒绝新提交的任务。 但得注意,当任务持续以平均提交速度大余平均处理速度时,会导致线程数量会无限增长问题。 - Unbounded queues 无界队列
当所有corePoolSize线程繁忙时,使用无界队列(例如,没有预定义容量的LinkedBlockingQueue)将导致新任务在队列中等待,从而导致maximumPoolSize的值没有任何作用。当每个任务互不影响,完全独立于其他任务时,这可能是合适的; 例如,在网页服务器中, 这种队列方式可以用于平滑瞬时大量请求。但得注意,当任务持续以平均提交速度大余平均处理速度时,会导致队列无限增长问题。 - Bounded queues 有界队列
一个有界的队列(例如,一个ArrayBlockingQueue)和有限的maximumPoolSizes配置有助于防止资源耗尽,但是难以控制。队列大小和maximumPoolSizes需要 相互权衡:
使用大队列和较小的maximumPoolSizes可以最大限度地减少CPU使用率,操作系统资源和上下文切换开销,但会导致人为的低吞吐量。如果任务经常被阻塞(比如I/O限制),那么系统可以调度比我们允许的更多的线程。
使用小队列通常需要较大的maximumPoolSizes,这会使CPU更繁忙,但可能会遇到不可接受的调度开销,这也会降低吞吐量。
这里主要为了说明有界队列大小和maximumPoolSizes的大小控制,若何降低资源消耗的同时,提高吞吐量
- ThreadFactory 线程工厂
新线程使用ThreadFactory创建。 如果未另行指定,则使用Executors.defaultThreadFactory默认工厂,使其全部位于同一个ThreadGroup中,并且具有相同的NORM_PRIORITY优先级和非守护进程状态。
通过提供不同的ThreadFactory,您可以更改线程的名称,线程组,优先级,守护进程状态等。如果ThreadCactory在通过从newThread返回null询问时未能创建线程,则执行程序将继续,但可能无法执行任何任务。
线程应该有modifyThread权限。 如果工作线程或使用该池的其他线程不具备此权限,则服务可能会降级:配置更改可能无法及时生效,并且关闭池可能会保持可终止但尚未完成的状态。 - Rejected tasks 拒绝任务
拒绝任务有两种情况:1. 线程池已经被关闭;2. 任务队列已满且maximumPoolSizes已满;
无论哪种情况,都会调用RejectedExecutionHandler的rejectedExecution方法。预定义了四种处理策略:
AbortPolicy:默认测策略,抛出RejectedExecutionException运行时异常;
CallerRunsPolicy:这提供了一个简单的反馈控制机制,可以减慢提交新任务的速度;
DiscardPolicy:直接丢弃新提交的任务;
DiscardOldestPolicy:如果执行器没有关闭,队列头的任务将会被丢弃,然后执行器重新尝试执行任务(如果失败,则重复这一过程);
我们可以自己定义RejectedExecutionHandler,以适应特殊的容量和队列策略场景中。
线程池线程启动方式
submit()和区别execute()?
- 接收的参数不一样
- submit有返回值,而execute没有
用到返回值的例子,比如说我有很多个做validation的task,我希望所有的task执行完,然后每个task告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。
然后我就可以把所有失败的原因综合起来发给调用者。
个人觉得cancel execution这个用处不大,很少有需要去取消执行的。
而最大的用处应该是第二点。 - submit方便Exception处理
意思就是如果你在你的task里会抛出checked或者unchecked exception,
而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。 - 底层
submit底调用的仍是excute
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
其中newTaskFor 这个方法是创建了RunnableFuture实现类 FutureTask (这个类实现了RunnableFuture 接口,RunnableFuture实现了Runable和Future接口),TutureTask 这个类内部封装了 Callable
Spring 的异步方法去执行(无返回值)
步骤:
- 在启动类或者配置类加上 @EnableAsync 注解.
- @Async注解可以用在方法上,也可以用在类上,用在类上,对类里面所有方法起作用.但要注意必须是其他的类对使用了@Async 注解的方法进行调用,自身调用的话不起作用
示例:
@Component
public class AsyncTest {
@Async
public void test(Integer s){
System.out.println(s);
}
}
@Autowired
AsyncTest asyncTest;
@GetMapping(value = "/test")
public void test() {
for (int i = 0; i < 10; i++) {
asyncTest.test(i);
}
}
结果
0
9
7
3
8
1
2
4
5
6
Spring的异步方法 + Future接收返回值
步骤:
@Async
public Future<Integer> test1(Integer i) {
System.out.println(i);
return new AsyncResult(i);
}
List<Future<Integer>> res = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Future<Integer> integerFuture = asyncTest.test1(i);
res.add(integerFuture);
}
List<Integer> integers = new ArrayList<>();
for (Future<Integer> re : res) {
integers.add(re.get());
}
for (Integer integer : integers) {
System.out.println(integer);
}
这是异步执行的结果
0
3
1
5
4
6
7
8
9
2
这是通过异步执行通过加一个List来获得顺序执行结果 (当时面试中国航信被问到过:如何顺序获得异步执行的结果)
0
1
2
3
4
5
6
7
8
9
原生Future方法
- Future保存异步计算的结果,可以在我们执行任务时去做其他工作,并提供了以下几个方法:
- cancel(boolean mayInterruptIfRunning):试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false
- isCancel():判断任务是否在正常执行完前被取消的,如果是则返回true
- isDone():判断任务是否已完成
- get():等待计算结果的返回(阻塞),如果计算被取消了则抛出
- get(long timeout,TimeUtil unit):设定计算结果的返回时间,如果在规定时间内没有返回计算结果则抛出TimeOutException
- 使用Future的好处:
获取任务的结果,判断任务是否完成,中断任务
Future的get方法很好的替代的了Thread.join或Thread,join(long millis)
Future的get方法可以判断程序代码(任务)的执行是否超时,如:
ry{
future.get(60,TimeUtil.SECOND);
}catch(TimeoutException timeout){
log4j.log("异常,将被取消!!");
future.cancel();
}
步骤:
//我们需要执行的代码1
Future future = longTimeMethod();
//我们需要执行的代码2
Integer result = future.get();
可以看到,我们调用longTimeMethod返回一个Future对象,然后处理“我们需要执行的代码2”,到了需要返回结果的时候直接调用future.get()便能获取到返回值。下面我们来看看longTimeMethod如何实现。
private Future longTimeMethod2() {
//创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//获取异步Future对象
Future future = threadPool.submit(new Callable() {
@Override
public Integer call() throwsException {
return longTimeMethod();
}
});
return future;
}