深入浅出JAVA线程池使用原理1

时间:2021-09-28 01:52:50

前言:

Java中的线程池是并发框架中运用最多的,几乎所有需要异步或并发执行任务的程序都可以使用线程池,线程池主要有三个好处:

1、降低资源消耗:可以重复使用已经创建的线程降低线程创建和销毁带来的消耗

2、提高响应速度:执行任务时,不需要等待线程的创建就可以直接执行任务

3、提高线程的可管理性:线程是稀缺资源,如果无限制地创建不仅会消耗系统资源,还会降低系统的稳定性,线程池可以对线程进行统一分配、调优和监控

一、线程池的实现原理

在了解线程池实现原理之前,先了解线程池的一些元素

1.核心线程池

线程池有一个核心线程池,核心线程池的大小在线程池创建时设定,默认是有任务提交时会创建线程来执行,也可以调用线程池的prestartAllCoreThreads来提前创建并启动所有的基本线程。

2.任务队列

当核心线程池中的线程数超过设置的的大小时,再有新的任务提交则会先将任务当道队列中,等待核心线程池中的线程执行当前的任务之后,再到队列中获取任务来执行

3.饱和策略

当线程池已经处于饱和状态,无法再分配线程给新的任务时,会采用饱和策略拒绝新的任务

1.1.线程池的工作流程

线程池的整体工作流程如下图示

深入浅出JAVA线程池使用原理1

当有新任务提交到线程池时,线程池的处理流程如下:

1.判断核心线程池是否已满,如果没满,则创建新线程来执行此任务(即使当前有空闲的线程也会直接创建,而不是使用空闲的线程来执行),直到核心线程池中的线程数达到了设置的大小之后就不再创建;

如果核心线程池已经满了,则进入下一阶段的判断

2.判断工作队列是否已经满了,如果工作队列没有满,则将任务暂时存放到工作队列中,等待核心线程池中的线程空闲下来再来获取任务执行(核心线程池中的线程执行完任务之后会循环从工作队列中取任务来执行);如果队列也满了,则进入下一阶段的判断

3.判断线程池的是否已满(线程池除了核心线程池,还设置了线程池的最大线程大小,即使核心线程池满了,还是可以再创建线程),如果线程池中工作的线程没有达到最大值,则创建新线程来执行任务;

如果线程池也已经满了, 则按照线程池的饱和策略来处理任务(具体怎么处理任务按具体的饱和策略来执行)

1.2、线程池的创建

创建线程池可以通过ThreadPoolExecutor来创建,构造方法如下

 public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

这里涉及到了7个参数,含义分别如下:

corePoolSize:核心线程池大小,核心线程池的用法上面已经提到,不再赘述

maximumPoolSize:线程池的最大数量,也就是线程池允许创建的最大线程数,如果线程池的工作队列满了,则会先判断是否达到了最大线程数,若没有则还可以再创建线程来执行新任务,直到线程数达到线程池的最大数量

keepAliveTime:保持线程活动时间,当核心线程池中的线程处于空闲状态时,不会立即销毁线程而是保持一定的活动时间来等待任务,一定程度上提高了线程的复用率。

unit:线程活动时间(keepAliveTime)的单位,可以选择DAYS(天)、(HOURS)时、(MINUTES)分、(SECONDS)秒、(MILLSECONDS)毫秒、(MICROSECONDS)微妙、(NANOSECONDS)纳秒

workQueue:工作队列,核心线程池满了,新任务会存放在工作队列中等待核心线程池中的线程来获取(后面会详细描述)

threadFactory:线程工厂,线程池可以设置指定的线程工厂来创建新线程。如果不设置,默认是使用Executors.defaultThreadFactory()来创建

handler:饱和策略,当线程池已经满了,则说明线程池已经处于饱和状态无法再接受新任务了,那么就采取饱和策略来处理新任务,默认使用AbortPolicy,表示无法处理新任务而直接抛出异常(后面会详细描述)

1.3、线程池提交任务

线程池有两个方法可以用来提交任务,分别是execute()和submit()

execute()方法用于提交不需要返回值的方法,所以无法判断任务是否被线程池执行成功

submit()方法用于提交需要返回值的方法,线程池会返回一个future类型的对象,通过future对象可以判断任务是否执行成功,并且可以通过get方法来获取线程执行的返回值,get方法会阻塞当前线程直到任务结束,也可以给get方法设置指定时长,

则达到指定时长之后会立即返回,但是此时可能线程还没有执行完。

1.4、关闭线程池

线程池关闭方法有shutDown方法和shutDownNow方法,原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,如果线程无法响应中断的任务就可能永远无法终止。

shutDown方法是将线程池的状态设置为SHUTDOWN状态,然后中断所有还没有执行的任务线程

showDownNow是将线程池中的状态设置为STOP,然后尝试停止所有正在执行或空闲的线程,并返回等待执行任务的列表

如果需要保证任务执行完,则建议使用shutDown方法来执行关闭方法

1.5、线程池的监控

线程池有多个属性可和方法来监控当前线程池的状态

taskCount:线程池需要执行的任务总数量(包括等待执行和已经执行完的)

completedTaskCount:线程池已完成的任务数量

largestPoolSize:线程池运行中创建的最大线程数,可以判断线程池运行过程中是否达到了最大线程数

getPoolSize:线程池中的线程数量

getActiveCount:获取活动的线程数

二、线程池工作队列和饱和策略

2.1、工作队列 (BlockingQueue<Runnable>),由名字可以看出线程池的工作队列是一个阻塞队列,主要有以下四种类型:

ArrayBlockingQueue:基于数组结构的有界阻塞队列,采用FIFO(先进先出)原则对任务进行排序

LinkedBlockingQueue:基于链表结构的阻塞队列,采用FIFO(先进先出)原则对任务进行排序,吞吐量要高于ArrayBlockingQueue

SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态,吞吐量高于LinkedBlockingQueue

PriorityBlockingQueue:一个具有优先级的无限阻塞队列

2.2、饱和策略(RejectedExecutionHandler)当线程池无法处理新任务时,就将采用饱和策略来拒绝任务的执行,主要有以下四种类型:

AbortPolicy:拒绝任务,抛出异常,也是线程池默认饱和策略

CallerRunsPolicy:拒绝任务,使用调用者的当前线程来执行此任务

DiscardOldestPolicy:丢弃工作队列中的最近一个任务,并处理当前任务

discardPolicy:不做任何处理,直接丢弃任务

三、线程池实战(测试案例)

先创建一个任务类,代码如下:

  public static class ThreadPoolTaskTest implements Runnable
{
private int taskIndex;//任务编号 public ThreadPoolTaskTest(int i)
{
taskIndex = i;
} @Override
public void run()
{
try
{
//线程睡眠2秒
Thread.sleep(2000);
System.out.println("线程:"+Thread.currentThread().getName()+"执行任务:"+taskIndex);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
} }
 public static void main(String[] args)
{
/**
* 1.创建线程池
* 核心线程池大小:2、 最大线程池大小:4、 工作队列:有界阻塞队列,队列大小为6 线程活动保持时间:10、 活动时间单位:毫秒、 线程工厂:默认、 饱和策略:默认
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(6));
threadPoolTest(executor);
} public static void threadPoolTest(ThreadPoolExecutor executor){
/**
* 前提:核心线程数为2,队列为6,而最大线程池为4
* 测试:向线程池中提交10个任务
* 猜想:核心线程池创建2个线程执行2个任务,6个任务放到队列中然后继续等待执行,2个任务被线程池创建的其他两个线程执行
* */
for (int i = 0; i < 10; i++)
{
executor.execute(new ThreadPoolTaskTest(i));
}
}

执行结果如下:

 线程:pool-1-thread-3执行任务:8
线程:pool-1-thread-1执行任务:0
线程:pool-1-thread-2执行任务:1
线程:pool-1-thread-4执行任务:9
线程:pool-1-thread-1执行任务:4
线程:pool-1-thread-4执行任务:5
线程:pool-1-thread-3执行任务:2
线程:pool-1-thread-2执行任务:3
线程:pool-1-thread-1执行任务:6
线程:pool-1-thread-4执行任务:7

当将任务数量增加到12个,则核心线程池2个,工作队列6个,再创建两个线程执行2个,还有2个任务将会被饱和策略直接拒绝,结果如下

 1 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.lucky.test.jvmtest.ThreadPoolTest$ThreadPoolTaskTest@33909752 rejected from java.util.concurrent.ThreadPoolExecutor@55f96302[Running, pool size = 4, active threads = 4, queued tasks = 6, completed tasks = 0]
2 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
3 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
4 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
5 at com.lucky.test.jvmtest.ThreadPoolTest.threadPoolTest1(ThreadPoolTest.java:32)
6 at com.lucky.test.jvmtest.ThreadPoolTest.main(ThreadPoolTest.java:21)
线程:pool-1-thread-4执行任务:9
线程:pool-1-thread-1执行任务:0
线程:pool-1-thread-3执行任务:8
线程:pool-1-thread-2执行任务:1
线程:pool-1-thread-2执行任务:5
线程:pool-1-thread-1执行任务:3
线程:pool-1-thread-3执行任务:4
线程:pool-1-thread-4执行任务:2
线程:pool-1-thread-1执行任务:7
线程:pool-1-thread-2执行任务:6

修改饱和策略,采用CallerRunsPolicy饱和策略,则其他十个任务正常执行,另外两个任务将会由提交任务的线程来执行任务,结果如下:

 线程:main执行任务:10
线程:main执行任务:11
线程:pool-1-thread-2执行任务:1
线程:pool-1-thread-3执行任务:8
线程:pool-1-thread-4执行任务:9
线程:pool-1-thread-1执行任务:0
线程:pool-1-thread-1执行任务:3
线程:pool-1-thread-4执行任务:4
线程:pool-1-thread-3执行任务:2
线程:pool-1-thread-2执行任务:5
线程:pool-1-thread-4执行任务:7
线程:pool-1-thread-1执行任务:6

总结:本文主要整理了线程池的基本知识点及大致用法,下篇将针对Executor框架的多种线程池分别整理