线程池基本介绍与使用

敏捷侠
• 阅读 1160

线程池基本介绍与使用

我们知道,在Java中,创建对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。

所以Java中提供了线程池,其本质就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

JDK线程池相关类

JDK中提供的有关线程池的相关类及其关系如下图:
线程池基本介绍与使用

  • Executor接口:线程池的抽象接口,只包含一个execute方法。
  • ExecutorService子接口:提供了有关终止线程池和Future返回值的一些方法。
  • AbstractExecutorService抽象类:提供了ExecutorService的一些默认实现。
  • ThreadPoolExecutor类:JDK提供的线程池的实现类。
  • Executors类:线程池工厂类,提供了几种线程池的工厂方法。

下面主要介绍JDK提供的线程池实现类 - ThreadPoolExecutor类。

ThreadPoolExecutor类

Java 提供的线程池相关的工具类中,最核心的是 ThreadPoolExecutor。

构造方法

ThreadPoolExecutor 的构造函数非常复杂,如下面代码所示,这个最完备的构造函数有 7 个参数。

ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 

下面一一介绍下参数的含义。

corePoolSize
  • 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,才会去创建一个线程来执行任务
  • 当线程池中的线程数目达到corePoolSize后,就停止线程创建,转而会把任务放到任务队列当中等待。
  • 调用prestartAllCoreThreads()或者prestartCoreThread()方法,可以预创建线程,即在没有任务到来之前就创建corePoolSize个线程或者一个线程
maxPoolSize
  • 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。
  • 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
keepAliveTime & unit

当线程空闲时间达到keepAliveTime,单位unit时,该线程会退出,直到线程数量等于corePoolSize。

workQueue

任务队列,一个阻塞队列,用来存储等待执行的任务,建议workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。

threadFactory

线程工厂,通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

handler

拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收,可以通过 handler 这个参数来指定拒绝的策略。ThreadPoolExecutor 已经提供了以下 4 种策略:

  • ThreadPoolExecutor.AbortPolicy:默认的拒绝策略。丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列等待最久的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。

常用方法

提交任务方法

void execute(Runnable command):提交任务给线程池执行,任务无返回值
Future<?> submit(Runnable task):由于Runnable接口没有返回值,所以Future返回值执行get()方法返回值为null,作用只是等待,类似于join
<T> Future<T> submit(Runnable task, T result):由于Runnable没有返回值,所以额外提供了一个参数,作为返回值。
<T> Future<T> submit(Callable<T> task):提交任务给线程池执行,能够返回执行结果。

其他方法

void allowCoreThreadTimeOut(boolean value):是否允许核心线程超时,默认false。
shutdown():关闭线程池,等待任务都执行完
shutdownNow():关闭线程池,不等待任务执行完,并返回等待执行的任务列表。
getTaskCount():线程池已执行和未执行的任务总数
getCompletedTaskCount():已完成的任务数量
getPoolSize():线程池当前的线程数量
getActiveCount():当前线程池中正在执行任务的线程数量

线程池任务的执行流程

线程池任务的一般执行流程图如下图所示:
线程池基本介绍与使用

线程池初始化示例

下面是一个线程池初始化的示例,仅供参考

// 初始化示例
private static final ThreadPoolExecutor pool;

static {
    ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("po-detail-pool-%d").build();
    pool = new ThreadPoolExecutor(
            4,
            8, 
            60L, 
            TimeUnit.MILLISECONDS, 
            new LinkedBlockingQueue<>(512),
            threadFactory, new ThreadPoolExecutor.AbortPolicy());
    pool.allowCoreThreadTimeOut(true);
}

初始化参数含义解释:

  • threadFactory:给出带业务语义的线程命名。
  • corePoolSize:快速启动4个线程处理该业务,是足够的。
  • maximumPoolSize:IO密集型业务,我的服务器是4C8G的,所以4*2=8。
  • keepAliveTime:服务器资源紧张,让空闲的线程快速释放。
  • pool.allowCoreThreadTimeOut(true):也是为了在可以的时候,让线程释放,释放资源。
  • workQueue:一个任务的执行时长在100~300ms,业务高峰期8个线程,按照10s超时(已经很高了)。10s钟,8个线程,可以处理10 1000ms / 200ms 8 = 400个任务左右,往上再取一点,512已经很多了。
  • handler:极端情况下,一些任务只能丢弃,保护服务端。

线程池使用注意事项

  • 避免使用Executors类创建线程池,会有OOM风险。
  • 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。即threadFactory参数要构造好。
  • 建议不同类别的业务用不同的线程池,至于线程池的数量,各自计算各自的,然后去做压测。
  • workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。
  • 如果是资源紧张的应用,使用allowsCoreThreadTimeOut可以提高资源利用率。
  • 虽然使用线程池有多种异常处理的方式,但在任务代码中,使用try-catch最通用,也能给不同任务的异常处理做精细化。
  • 线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
  • CPU密集型任务,最大线程数初始值可以配置N+1。I/O密集型任务,最大线程数初始值可以配置2N。之后再可以根据压测来调整。

参考资料

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
java_线程池
血一样的教训,今天上午参加了一家现场面试java。在这之前,我一直认为我的java基础还是可以的,而今天一问三不知。现在将面试的问题整理出来一、说说java中的线程池?  1.线程池:线程池是线程的集合,不用自己创建线程,把线程直接给线程池,由线程池处理。   2.过程:首先,使用线程池可以重复利用已有的线程继续执行任务,避免线程在
Wesley13 Wesley13
3年前
java各种面试问题
二、Java多线程相关线程池的原理,为什么要创建线程池?创建线程池的方式;线程的生命周期,什么时候会出现僵死进程;说说线程安全问题,什么实现线程安全,如何实现线程安全;创建线程池有哪几个核心参数?如何合理配置线程池的大小?volatile、ThreadLocal的使用场景和原理;
Wesley13 Wesley13
3年前
java 线程池入门
一简介线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的。在jdk1.5之后这一情况有了很大的改观。Jdk1.5之后加入了java.util.concurrent包,这个包中主要介绍java中线程以及线程池的使用。为我们在开发中处理线程的问题提供了非常大的帮助。二:线程池
Wesley13 Wesley13
3年前
java四大线程池
一、为什么需要使用线程池  1、减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。2、可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。Java中创建和销毁一个线程是比较
zdd小小菜鸟 zdd小小菜鸟
2年前
多线程面试
多线程篇1.为什么要使用线程池tex避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。2.java中如何获取到线程dump文件tex死循环、死锁、阻
Wesley13 Wesley13
3年前
Java基础教程——线程池
启动新线程,需要和操作系统进行交互,成本比较高。使用线程池可以提高性能——线程池会提前创建大量的空闲线程,随时待命执行线程任务。在执行完了一个任务之后,线程会回到空闲状态,等待执行下一个任务。(这个任务,就是Runnable的run()方法,或Callable的call()方法)。Java5之前需要手动实现线程池,Java5之
Wesley13 Wesley13
3年前
Java 线程池原理分析
1.简介线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销。在应用上,线程池可应用在后端相关服务中。比如Web服务器,数据库服务器等。以Web服务器为例,假如Web服务器会收到大量短时的HTTP请求,如果此时我们简单的为每个HTTP请求创建一个处理线程,那么服务器
Stella981 Stella981
3年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
Wesley13 Wesley13
3年前
Java中的线程池
java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理使用线程池能够带来三个好处。第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。第三:提高线程的可管理性。线程是稀缺
ThreadPoolExecutor线程池内部处理浅析 | 京东物流技术团队
我们知道如果程序中并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束时,会因为频繁创建线程而大大降低系统的效率,因此出现了线程池的使用方式,它可以提前创建好线程来执行任务。本文主要通过java的ThreadPoolExecutor来查看线程池
敏捷侠
敏捷侠
Lv1
保持对生活的爱和热忱,把每一天活得热气腾腾。
文章
4
粉丝
0
获赞
0