Java线程池看这一篇就够了


关联文章:

一、HashMap看这一篇就够了
二、synchronized看这一篇就够了
三、HTTP协议看这一篇就够了

一、线程池基础

1、什么是线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后再需要执行新的任务时重复使用这些线程而不是新建线程,从而降低创建和销毁线程的消耗。

2、为什么使用线程池

创建Java线程需要给线程分配堆栈内存以及初始化内存并且需要进行系统状态的切换,而且频繁地创建和销毁线程会大大降低系统的运行效率,采用线程池来管理线程有以下好处:

  1. 提升性能:线程池能独立负责线程的创建、维护和分配
  2. 线程管理:每个Java线程池会保持一些基本的线程统计信息,对线程进行有效管理

3、线程池使用场景

java中经常需要用到多线程来处理一些业务,一般开发规范中都禁止单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。使用线程池一般有如下两方面的要求:

  1. 加快请求响应(响应时间优先)
  2. 加快处理大任务(吞吐量优先)

二、线程池使用

1、线程池的创建及重要参数

1.1、自动创建线程池

线程池可以自动创建也可以手动创建,自动创建主要使用的是Executors工具类:

// 创建固定数量的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

// 基于ForkJoin框架的任务窃取线程池,任务窃取可以理解为:当线程发现自己的队列没有任务了,就会到别的线程的队列里获取任务执行
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool(parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
 
 // 基本同newFixedThreadPool一样,但是将线程数设置为了1,单线程
public static ExecutorService newSingleThreadExecutor() {
	return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
 
//  corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;意味这它是一个可以无限扩大的线程池;
//  它比较适合处理执行时间比较小的任务;采用SynchronousQueue这个没有存储空间的阻塞队列,意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。
public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

从上述源码中可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是采用无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步队列,目的就是来一个任务就创建一个线程来执行任务,而不用队列来存储任务;

1.2、手动创建线程池(推荐)

在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:
在这里插入图片描述
Java内置线程池:ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务;
  • maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize);
  • keepAliverTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  • unit:keepAliveTime的时间单位;
  • workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中;
  • threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建;
  • handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy(中断抛出异常)、CallerRunsPolicy(让提交任务的线程去执行任务)、DiscardOldestPolicy(丢弃掉在队列中存在时间最久的任务)、DiscardPolicy(新任务就会直接被丢掉,并且不会有任何异常抛出);

关闭线程池

  • shutdownNow():立即关闭线程池(不推荐),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表;
  • shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略;
  • isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

2、向线程池提交任务的两种方式

2.1、execute方法

void execute(Runnable command): Executor接口中的方法;

2.2、submit方法

Future submit(Callable task);
Future submit(Runnable task, T result);
Future<?> submit(Runnable task);
这3个submit方法都是ExecutorService接口中的方法;

2.3、两种方法的区别:

  • execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数;
  • Callable类型的任务是可以返回执行结果的,而Runnable类型的任务不可以返回执行结果;
  • submit()提交任务后会有返回值,而execute()没有;
  • submit()方便Exception处理;

3、workQueue队列

  • SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
  • LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
  • ArrayBlockingQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
  • PriorityBlockingQueue:是具有优先级的无界队列

三、线程池的任务调度流程

在这里插入图片描述

  1. 如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程;
  2. 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满;
  3. 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空;
  4. 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务;
  5. 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略;

四、调度器的钩子方法

三个钩子方法存在于ThreadPoolExecutor类,这3个方法都是空方法,一般会在子类中重写
protected void beforeExecute(Thread t, Runnable r) { }: 任务执行之前的钩子方法
protected void afterExecute(Runnable r, Throwable t) { }: 任务执行之后的钩子方法
protected void terminated() { }: 线程池终止时的钩子方法

ExecutorService pool = new ThreadPoolExecutor(1, 4, 120, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2)) {
	@Override
	protected void terminated() {
		System.out.println("调度器已停止...");
	}

	@Override
	protected void beforeExecute(Thread t, Runnable target) {
		System.out.println("前钩执行...");
		super.beforeExecute(t, target);
	}

	@Override
	protected void afterExecute(Runnable target, Throwable t) {
		System.out.println("后钩执行...");
		super.afterExecute(target, t);
	}
};

五、Future 和 CompletableFuture

1、Future

1.1、Future接口

  • Future接口定义了操作异步任务执行的一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其他事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态;
  • Future是Java5新加的一个接口,它提供了一种异步并行计算的功能。如果主线程需要执行一个很耗时的计算任务,我们就可以通过Future把这个任务放到异步线程中执行。主线程继续处理其他任务或者先行结束,再通过Future获取计算结果;
  • future+线程池异步多线程任务配合,能显著提高程序的执行效率;

1.2、Future 的常用方法如下

  • boolean cancel(boolean mayInterruptIfRunning)
    试图取消对此任务的执行。如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败。当调用 cancel 时,如果调用成功,而此任务尚未启动,则此任务将永不运行。如果任务已经启动,则 mayInterruptIfRunning 参数确定是否应该以试图停止任务的方式来中断执行此任务的线程;
  • V get()
    如有必要,等待执行完成,然后获取其返回结果;
  • boolean isDone()
    如果任务已完成,则返回 true;

1.3、Future缺陷

  1. get()方法在Future计算完成之前会一直处在阻塞状态下,一旦调用必须要等到返回结果才会离开,很容易造成程序堵塞。Future对于结果的获取不是很友好,只能通过阻塞或者轮询的方式得到任务的结果;
  2. 阻塞的方式和异步编程的设计理念相违背,而isDone()轮询的方式会消耗无谓的CPU资源,而且也不见的能及时地得到计算结果;
  3. 无法对多个任务进行链式调用:如果你希望在计算任务完成后执行特定动作,比如发邮件等,但Future却没有提供这样的能力;
  4. 没有异常处理:Future接口中没有关于异常处理的方法;

2、CompletableFuture

1.1、CompletableFuture简介

CompletableFuture是java.util.concurrent库在java 8中新增的主要工具,同传统的Future相比,其支持流式计算、函数式编程、完成通知、自定义异常处理等很多新的特性;

CompletableFuture实现了CompletionStage接口和Future接口,CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段一个阶段的计算执行可以是一个Function,Consumer或者Runnable,使Java在处理多任务的协同工作时更加顺畅便利;

CompletableFuture 和 FutureTask 同属于 Future 接口的实现类,都可以获取线程的执行结果;而CompletableFuture实现了对任务编排的能力,借助这项能力,可以轻松地组织不同任务的运行顺序、规则以及方式。从某种程度上说,这项能力是它的核心能力。而在以往,虽然通过CountDownLatch等工具类也可以实现任务的编排,但需要复杂的逻辑处理,不仅耗费精力且难以维护;

CompletableFuture中默认线程池如下:

// 根据commonPool的并行度来选择,而并行度的计算是在ForkJoinPool的静态代码段完成的
private static final boolean useCommonPool = (ForkJoinPool.getCommonPoolParallelism() > 1);

private static final Executor asyncPool = useCommonPool ?  ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

1.2、CompletableFuture常用方法总结

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值