线程池的理解以及使用 - Java技术债务

文章目录


1、线程池的引入

  如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。   那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

2、好处

1、降低资源消耗;通过重复利用已创建的线程降低创建和销毁造成的消耗。

2、提高响应速度;任务到达时,不用等待线程创建便能立刻执行。

3、提高线程的可管理性;统一监控、管理、调优。

3、简单剖析内存结构

  1. java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。
  2. 当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。
  3. 当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。 线程池的理解以及使用 - Java技术债务

4、核心参数

  1. corePoreSize核心线程数量:核心线程数线程数定义了最小可以同时运行的线程数量。
  2. maximumPoolSize最大线程数量 : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。
  3. workQueue任务队列 : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。
  4. keepAliveTime线程存活保持时间 :当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任 务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;如果allowCoreThreadTimeout=true,则会直到线程数量=0。
  5. unit 时间格式 : keepAliveTime 参数的时间单位。
  6. threadFactory线程工厂 :executor 创建新线程的时候会使用到。
  7. handler 线程饱和策略 :饱和策略。
  8. allowCoreThreadTimeout:允许核心线程超时
  9. rejectedExecutionHandler:任务拒绝处理器;两种情况会拒绝处理任务:当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。

饱和策略:

ThreadPoolExecutor 饱和策略定义: 当线程池和队列都满了,再加入线程会执行策略。 ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。 ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加 队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。

5、线程池添加任务流程

线程池的理解以及使用 - Java技术债务

6、线程池参数配置依据

核心参数的配置依据根据网上最具可靠性的结果:

核心线程数配置依据:

1、判断当前线程池处理的程序是属于cpu密集型还是IO密集型

CPU密集型:CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading 很高。大部分时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

IO密集型:指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。

**CPU密集型:**线程的平均工作时间所占比例越高,就需要越少的线程;

corePoolSize = CPU核数 + 1

**IO密集型:**线程的平均等待时间所占比例越高,就需要越多的线程;

corePoolSize = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)

CPU 核心数 / (1 - 阻塞系数)

7、线程池队列的选择

BlockingQueue阻塞队列:无界队列有界队列同步移交

无界队列:队列大小不限制,常用LinkedBlockingQueue,还有DelayedWorkQueue;当任务耗时较长可能会导致大量新任务在队列中堆积,最后导致OOM;最大线程数的值会失效,

有界队列:有效防止资源耗尽,常用两大类:一遵循FIFO原则的队列ArrayBlockingQueue,二优先级队列PriorityBlockingQueue;使用有界队列需要和线程池大小配合,线程池较小,有界队列较大时会减少内存消耗,见地CPU使用率和上下文切换,但是会限制吞吐量。

同步移交队列:如果不希望任务在队列中等待,直接交给工作线程,使用SynchronizedQueue作为等待队列,并不是真正队列,而是线程之间移交的机制,

只有在使用无界线程池或者有饱和策略时才建议使用该队列。

8、线程池回收线程

1、runWorker(Worker worker)

工作线程启动后,进入runWorker()方法,while循环判断任务是否为空,不为空,继续执行任务,若取不到任务或者发生异常,退出循环,执行processWorkerExit(w, completedAbruptly); 在这个方法里把工作线程移除掉。

取工作任务来源有两个,一个是firstTask,这个工作线程是第一次跑的时候执行的任务,最多只能执行一次,后面的任务从getTask()方法获取。方法返回null退出循环。

2、getTask()返回null的情况:

一:线程池状态是stop、tidying、terminated或者是shutdown且工作对列为空。

二:工作线程大于最大线程或者当前线程已超时,且还有其他工作线程或工作队列为空,

3、分场景分析线程池回收工作线程

一、未调用shutdown(),running状态下的任务全部执行完成。将工作线程减少到核心线程数大小(如果没有超过核心线程数,则不用回收)取决于allowCoreThreadTimeOut的值,这里讨论默认值false的情况,即核心线程不会超时。如果为true,工作线程可以全部销毁

二、调用shutdown() ,全部任务执行完成的场景

无论是核心线程还是非核心线程,所有工作线程都会被销毁。在调用shutdown()之后,会向所有的空闲工作线程发送中断信号。在发出中断信号前,会判断是否已经中断,以及要获得工作线程的独占锁。

总结:

ThreadPoolExecutor回收工作线程,一条线程getTask()返回null,就会被回收。

分两种场景。

1.**未调用shutdown() **,RUNNING状态下全部任务执行完成的场景

线程数量大于corePoolSize,线程超时阻塞,超时唤醒后CAS减少工作线程数,如果CAS成功,返回null,线程回收。否则进入下一次循环。当工作者线程数量小于等于corePoolSize,就可以一直阻塞了。

2.**调用shutdown() **,全部任务执行完成的场景

shutdown() 会向所有线程发出中断信号,这时有两种可能。

** 2.1)所有线程都在阻塞**

中断唤醒,进入循环,都符合第一个if判断条件,都返回null,所有线程回收。

2.2)任务还没有完全执行完

至少会有一条线程被回收。在processWorkerExit(Worker w, boolean completedAbruptly)方法里会调用tryTerminate(),向任意空闲线程发出中断信号。所有被阻塞的线程,最终都会被一个个唤醒,回收

   登录后才可以发表评论呦...

专注分享Java技术干货,包括
但不仅限于多线程、JVM、Spring Boot
Spring Cloud、 Redis、微服务、
消息队列、Git、面试题 最新动态等。

想交个朋友吗
那就快扫下面吧


微信

Java技术债务

你还可以关注我的公众号

会分享一些干货或者好文章

Java技术债务