热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【多线程】说说线程池

前言线程池内部是多个线程的集合,在创建初期,线程池会创建出多个空闲的线程,当有一个任务需要执行时,线程池会选择出一个线程去

前言

线程池内部是多个线程的集合,在创建初期,线程池会创建出多个空闲的线程,当有一个任务需要执行时,线程池会选择出一个线程去执行它,执行结束后,该线程不会被销毁,而是可以继续复用。

使用线程池可以大大减少线程频繁创建与销毁的开销,降低了系统资源的消耗。当任务来临时,直接复用之前的线程,而不是先创建,提高了系统的响应速度。此外,线程池可以控制最大的并发数,避免资源的过度消耗。




简单实例

先给出一个线程池的简单例子:

package com.xue.testThreadPool;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class Main {public static void main(String[] args) {ExecutorService threadPool = Executors.newFixedThreadPool(2);for (int i = 0; i <4; i++) {int finalI = i;threadPool.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "正在执行任务" + finalI);}});}threadPool.shutdown();}
}

输出如下:

可见,2个线程总共执行了4个任务,线程得到了复用。




线程池的核心参数

这些核心参数位于ThreadPoolExecutor的构造方法中:

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

  • corePoolSize               核心线程数,或者说常驻线程数,线程池中最少线程数
  • maximumPoolSize      最大线程数
  • keepAliveTime             空闲线程的存活时间,线程池中当前线程数大于corePoolSize时,那些空闲时间达到keepAliveTime的空闲线程,它们将会被销毁掉
  • TimeUnit                       keepAliveTime的时间单位
  • workQueue                   任务队列,存放未被执行的任务
  • threadFactory               创建线程的工厂
  • handler                          拒绝策略,当前线程数≥最大线程数且任务队列满的时候,对后续任务的拒绝方式



线程池的种类

不同的线程池有不同的适用场景,本质上都是在Executors类中实例化一个ThreadPoolExecutor对象,只是传入的参数不一样罢了。

线程池的种类有以下几种:


newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue());}

创建一个固定大小的线程池,即核心线程数等于最大线程数,每个线程的存活时间和线程池的寿命一致,线程池满负荷运作时,多余的任务会加入到无界的阻塞队列中,newFixedThreadPool可以很好的控制线程的并发量。


newCachedThreadPool

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue());}

创建一个可以无限扩大的线程池,当任务来临时,有空闲线程就去执行,否则立即创建一个线程。当线程的空闲时间超过1分钟时,销毁该线程。适用于执行任务较少且需要快速执行的场景,即短期异步任务。


newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()));}

创建一个大小为1的线程池,用于顺序执行任务。


newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);}public ScheduledThreadPoolExecutor(int corePoolSize) {super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());}

创建一个初始大小为corePoolSize的线程池,线程池的存活时间没有限制,newScheduledThreadPool中的schedule方法用于延时执行任务,scheduleAtFixedRate用于周期性地执行任务。




 线程池执行任务的流程


  • 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。

  • 当线程池中线程数达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 。

  • 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程执行任务。

  • 当workQueue已满,且提交任务数超过maximumPoolSize,任务由RejectedExecutionHandler处理。

  • 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。

  • 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。


使用更加直观的流程图来描述:

注:此章节参考通俗易懂,各常用线程池执行的-流程图




工作队列

工作队列用来存储提交的任务,工作队列一般使用的都是阻塞队列。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。

阻塞队列一般由以下几种:


LinkedBlockingQueue  

由单链表实现的无界阻塞队列,遵循FIFO。注意这里的无界是因为其记录队列大小的数据类型是int,那么队列长度的最大值就是恐怖的Integer.MAX_VALUE,这个值已经很大了,因此可以将之称为无界队列。不过该队列也提供了有参构造函数,可以手动指定其队列大小,否则使用默认的int最大值。

LinkedBlockingQueue只能从head取元素,从tail添加元素。添加元素和获取元素都有独立的锁,也就是说它是读写分离的,读写操作可以并行执行。LinkedBlockingQueue采用可重入锁(ReentrantLock)来保证在并发情况下的线程安全。

当线程数目达到corePoolSize时,后续的任务会直接加入到LinkedBlockingQueue中,在不指定其队列大小的情况下,该队列永远也不会满,可能内存满了,队列都不会满,此时maximumPoolSize和拒绝策略将不会有任何意义


ArrayBlockingQueue

由数组实现的有界阻塞队列,同样遵循FIFO,必须制定队列大小。使用全局独占锁的方式,使得在同一时间只有一个线程能执行入队或出队操作,相比于LinkedBlockingQueue,ArrayBlockingQueue锁的力度很大。


SynchronousQueue

是一个没有容量的队列,当然也可以称为单元素队列。会将任务直接传递给消费者,添加任务时,必须等待前一个被添加的任务被消费掉,即take动作等待put动作,put动作等待take动作,put与take是循环往复的

如果线程拒绝执行该队列中的任务,或者说没有线程来执行。那么旧任务无法被执行,新任务也无法被添加,线程池将陷入一种尴尬的境地。因此,该队列一般需要maximumPoolSize为Integer.MAX_VALUE,有一个任务到来,就立马新起一个线程执行,newCachedThreadPool就是使用的这种组合。

关于这些阻塞队列的源码解析,可能需要另开篇幅。




线程工厂

先看一下,ThreadPoolExecutor构造方法中默认使用的线程工厂

static class DefaultThreadFactory implements ThreadFactory {private static final AtomicInteger poolNumber = new AtomicInteger(1);private final ThreadGroup group;private final AtomicInteger threadNumber = new AtomicInteger(1);private final String namePrefix;DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();namePrefix = "pool-" +poolNumber.getAndIncrement() +"-thread-";}public Thread newThread(Runnable r) {Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon())t.setDaemon(false);if (t.getPriority() != Thread.NORM_PRIORITY)t.setPriority(Thread.NORM_PRIORITY);return t;}}

defaultThreadFactory对于线程的命名方式为“pool-”+pool的自增序号+"-thread-"+线程的自增序号,这也印证了在简单实例的章节中,输出Thread.getCurrentThread.getName()是“pool-1-thread-1”的样式

默认线程工厂给线程的取名没有太多的意义,在实际开发中,我们一般会给线程取个比较有识别度的名称,方便出现问题时的排查。




拒绝策略

如果当工作队列已满,且线程数目达到maximumPoolSize后,依然有任务到来,那么此时线程池就会采取拒绝策略。

ThreadPoolExecutor中提供了4种拒绝策略。


AbortPolicy

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); public static class AbortPolicy implements RejectedExecutionHandler {public AbortPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {throw new RejectedExecutionException("Task " + r.toString() +" rejected from " +e.toString());}}

这是线程池的默认拒绝策略,直接会丢弃任务并抛出RejectedExecutionException异常。


DiscardPolicy

public static class DiscardPolicy implements RejectedExecutionHandler {public DiscardPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}}

丢弃后续提交的任务,但不抛出异常。建议在一些无关紧要的场景中使用此拒绝策略,否则无法及时发现系统的异常状态。


DiscardOldestPolicy

public static class DiscardOldestPolicy implements RejectedExecutionHandler {public DiscardOldestPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {e.getQueue().poll();e.execute(r);}}}

从源码中可以看到,此拒绝策略会丢弃队列头部的任务,然后将后续提交的任务加入队列中。


CallerRunsPolicy

public static class CallerRunsPolicy implements RejectedExecutionHandler {public CallerRunsPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {r.run();}}}

由调用线程执行该任务,即提交任务的线程,一般是主线程。




如何配置最大线程数


CPU密集型任务

CPU密集指的是需要进行大量的运算,一般没有什么阻塞。

尽量使用较小的线程池,大小一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。


IO密集型任务

IO密集指的是需要进行大量的IO,阻塞十分严重,可以挂起被阻塞的线程,开启新的线程干别的事情。

可以使用稍大的线程池,大小一般为CPU核心数*2。IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

当然,依据IO密集的程度,可以在两倍的基础上进行相应的扩大与缩小。




总结

这篇文章粗浅地说明了线程池的种类、执行流程、工作队列与拒绝策略等,但缺少对线程池源码的分析,这个会另开篇幅进行说明。


推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • JavaSE笔试题-接口、抽象类、多态等问题解答
    本文解答了JavaSE笔试题中关于接口、抽象类、多态等问题。包括Math类的取整数方法、接口是否可继承、抽象类是否可实现接口、抽象类是否可继承具体类、抽象类中是否可以有静态main方法等问题。同时介绍了面向对象的特征,以及Java中实现多态的机制。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java自带的观察者模式及实现方法详解
    本文介绍了Java自带的观察者模式,包括Observer和Observable对象的定义和使用方法。通过添加观察者和设置内部标志位,当被观察者中的事件发生变化时,通知观察者对象并执行相应的操作。实现观察者模式非常简单,只需继承Observable类和实现Observer接口即可。详情请参考Java官方api文档。 ... [详细]
  • Spring学习(4):Spring管理对象之间的关联关系
    本文是关于Spring学习的第四篇文章,讲述了Spring框架中管理对象之间的关联关系。文章介绍了MessageService类和MessagePrinter类的实现,并解释了它们之间的关联关系。通过学习本文,读者可以了解Spring框架中对象之间的关联关系的概念和实现方式。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了闭包的定义和运转机制,重点解释了闭包如何能够接触外部函数的作用域中的变量。通过词法作用域的查找规则,闭包可以访问外部函数的作用域。同时还提到了闭包的作用和影响。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • CEPH LIO iSCSI Gateway及其使用参考文档
    本文介绍了CEPH LIO iSCSI Gateway以及使用该网关的参考文档,包括Ceph Block Device、CEPH ISCSI GATEWAY、USING AN ISCSI GATEWAY等。同时提供了多个参考链接,详细介绍了CEPH LIO iSCSI Gateway的配置和使用方法。 ... [详细]
author-avatar
拍友2502904975
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有