Java并发编程初识-线程池

Posted 敲代码的程序狗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程初识-线程池相关的知识,希望对你有一定的参考价值。

0. 前言

Java 中线程池是运用场景最多的并发框架,合理使用线程池可以带来诸多好处:

  • 降低资源消耗:重复利用已创建的线程,降低了线程创建和销毁时造成的资源消耗
  • 提高响应速度:任务到达时,不需要等待线程创建即可立即执行
  • 提高线程的可管理性:线程的无限创建不仅消耗系统资源,还会降低系统的稳定性,可以通过线程池统一分配、调优和监控

1. 线程池的实现原理

1.1 线程池处理任务

向线程池提交一个任务时,主要处理流程:

  • Step1:线程池判断核心线程池是否已满。如果没满则创建新线程来执行任务,如果满了则进入下个流程
  • Step2:线程池判断任务队列是否已满。如果没满则将任务添加进任务队列,如果满了则进入下个流程
  • Step3:线程池判断线程池是否已满。如果没满则创建新线程来执行任务,如果满了则交给拒绝策略处理

1.2 execute 方法的执行

ThreadPoolExecutor 执行 execute() 方法时,主要处理流程:

  • Step1:如果运行的线程数小于 corePoolSize,则创建新线程来执行任务(需要获取全局锁)
  • Step2:如果运行的线程数大于等于 corePoolSize,则将任务添加进 BlockingQueue
  • Step3:如果无法将任务添加进 BlockingQueue(队列已满),则创建新线程来执行任务(需要获取全局锁)
  • Step4:如果创建新线程将使当前运行的线程数超过 maximumPoolSize ,任务将被拒绝,并调用 RejectedExecutionHandler 的 rejectedExecution() 方法

ThreadPoolExecutor 采用上述步骤处理任务,是为了尽可能避免获取全局锁(那将会是一个严重的可伸缩瓶颈)

1.3 Worker 工作线程

线程池创建线程时,会将线程封装成工作线程 Worker ,工作线程执行完初始任务后,会从任务队列循环获取任务执行

工作线程执行任务的两种情况:

  • execute() 方法创建线程时,会让该线程执行当前任务
  • 线程执行完步骤1的初始任务后,会循环从 BlockingQueue 中获取任务执行

2. 线程池的使用

2.1 线程池的创建

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
复制代码
  • corePoolSize:核心线程数

  • maximumPoolSize:最大线程数,如果线程池使用了无界的任务队列,该参数失效

  • keepAliveTime:线程存活时间,工作线程空闲(任务队列无任务可获取)时,保持存活的时间

  • unit:线程存活时间的单位

  • workQueue:任务队列,保存等待执行的任务的阻塞队列

    • ArrayBlockingQueue:基于数组结构的有界阻塞队列,按 FIFO 原则排序元素

    • LinkedBlockingQueue:基于链表的无界阻塞队列,按 FIFO 原则排序元素。吞吐量高于 ArrayBlockingQueue 队列,Executors.newFixedThreadPool() 方法使用了该队列

    • SynchronousQueue:不存储元素的队列,每次插入操作必须等到另一个线程移除操作,否则插入操作一直阻塞。吞吐量高于 LinkedBlockingQueue 队列,Executors.newSingleThreadExecutor() 方法使用了该队列

    • PriorityBlockingQueue:具有优先级的无界阻塞队列

  • threadFactory:创建线程的工厂

  • handler:拒绝策略,当工作线程和任务队列都满时需要采取一种策略处理提交的任务

    • AbortPolicy:直接抛出异常
    • CallerRunsPolicy:只用调用者所在线程来执行任务
    • DiscardOldestPolicy:丢弃任务队列最近一个任务,然后调用 execute() 方法处理任务
    • DiscardPolicy:不处理,丢弃任务

2.2 向线程池提交任务

  • execute() 方法:提交不需要返回值的任务
  • submit() 方法:提交需要返回值的任务,可以通过下列方法获取返回值
    • Future.get() 方法:阻塞调用者所在线程直到任务执行完成返回结果
    • Future.get(long timeout, TimeUnit unit) 方法:阻塞调用者所在线程,超过指定时间立即返回,任务可能未执行完成

2.3 关闭线程池

  • shutdown() 方法:将线程池的状态设置为 SHUTDOWN ,尝试中断空闲的线程
  • shutdownNow() 方法:将线程池的状态设置为 STOP ,尝试停止所有线程(执行任务中、空闲)

2.4 合理的配置线程池

合理配置线程池的依据:

  • 任务的性质:
    • CPU 密集型:尽可能少的线程,如 cpuNum + 1 个线程
    • IO 密集型:尽可能多的线程,如 cpuNum * 2 个线程
    • 混合型:如果任务可拆分,拆分成一个 CPU 密集型和一个 IO 密集型任务。如果两个任务执行时间差距小,则分解后执行的吞吐量将高于串行执行。如果两个任务执行时间差距大,则没必要分解
  • 任务的优先级:高、中、低
    • 优先级不同的任务可以使用优先级任务队列处理(可能存在优先级小的任务一直无法执行)
  • 任务的执行时间:长、中、短
    • 执行时间不同的任务可以交给不同规模的线程池处理,也可以使用优先级任务队列处理,让执行时间短的任务先执行
  • 任务的依赖性:是否依赖其他系统资源,如数据库连接
    • 依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,等待时间越长 CPU 空闲时间越长,那么可以设置更多的线程以充分利用 CPU 资源

建议使用有界队列,增加系统的稳定性和预警能力,避免因线程阻塞导致任务无限堆积,最终可能出现系统因内存不足而不可用(OOM)

2.5 线程池的监控

如果系统中大量使用线程池,则有必要对线程池进行监控。线程池监控相关方法:

  • getTaskCount() 方法:获取待执行的任务数
  • getCompletedTaskCount() 方法:获取已执行完成的任务数
  • getLargestPoolSize() 方法:获取曾经创建过的最大线程数
  • getPoolSize() 方法:获取工作线程数
  • getActiveCount() 方法:获取活跃(执行任务中)的线程数

还可以通过继承线程池来自定义线程池,重写 beforeExecute()afterExecute()terminated() 方法,也可以在任务执行前后、线程池关闭前执行一些代码来进行监控

以上是关于Java并发编程初识-线程池的主要内容,如果未能解决你的问题,请参考以下文章

转:Java并发编程之十九:并发新特性—Executor框架与线程池(含代码)

Java并发编程:线程池 - 实例

Java核心知识---初识线程

Java并发多线程编程——线程池

并发编程精华问答| Java线程池使用时注意事项

并发编程精华问答| Java线程池使用时注意事项