线程池及其运行原理

前言

  • 首先从结构说起
  • 然后线程池的参数
  • 最后在结合代码简单分析

new Thread 弊端

        第一:每次new Thread 新建对象,性能差
        第二:线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM
        第三:缺少更多的功能,如更多执行、定期执行、线程中断。


什么是线程池

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序,都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
           第一:降低资源消耗。重用存在的线程,减少对象创建、消亡的开销,性能佳。
           第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
           第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用、


线程池实例的几种状态

           running状态:能接受新提交的任务,并且能处理阻塞队列中的任务。
           shutdown状态:当一个线程池实例处于关闭状态的时候,不能在接收新提交的任务,但是可以继续处理阻塞队列中已经保存的任务。在线程池处理running状态时,它调用shutdown方法,会使线程池进入到该状态。
           stop状态:不能接收新的任务,也不处理队列中的任务。它会中断正在处理中的线程,在线程池处于running或shutdown状态时,如果调用shutdownNow的时候会使线程池进入到该状态
           tidying状态:如果所用的任务都终止了,有效线程数为0,线程池会进入到该状态。之后调用terminated()方法会进入terminated状态。
           terminated状态


线程池的体系结构

根目录是Executor在JUC包下(java.util.concurrent),结构如下(这里说的是在JUC包下的子类):

|- Executor :负责线程的使用与调用的根接口
    |--** ExecutorService    子接口:线程池的主要接口
        |-- AbstractExecutorService 提供 ExecutorService 执行方法的默认实现
              |-- DelegatedExecutorService 
              |-- ForkJoinPool
              |-- ThreadPoolExecutor    线程池的实现类
        |-- ScheduledExecutorService    子接口:负责线程的调度
              |-- ScheduledThreadPoolExecutor:继承了 ThreadPoolExecutor,实现了ScheduledExecutorService

实际上最根本用的是ExecutorService,又因为ExecutorService是接口,接口不能创建对象,所以根本用的就是创建ThreadPoolExecutor的实例或ScheduledThreadPoolExecutor的实例。

但是基本上都是使用Executors工具类。最常见的有如下四个。但是这四个类都有可能OOM,一般情况下都需要根据自己需求,自定义线程池。

1、newFixedThreadPool()   :  创建固定大小的线程池,返回类型是ExecutorService。
2、newCachedThreadPool() :  缓存线程池,线程池的数量不固定,可以根据需求自动的更改数量。返回类型是ExecutorService。
3、newSingleThreadExecutor() :   创建单个线程池,线程池中只有一个线程。返回类型是ExecutorService。
4、newScheduledThreadPool()  :  创建固定大小的线程池,可以延迟或定时的执行任务。返回类型ScheduledExecutorService。

ThreadPoolExecutor常用的方法

  • execute():提交任务,交给线程池执行。
  • submit():提交任务,能够返回执行结果 execute + Future
  • shutdown():关闭线程池,等待任务都执行完
  • shutdownNow():关闭线程池,不等待任务执行完
  • getTaskCount():线程池已执行和未执行的任务总数
  • getCompletedTaskCount():已完成的任务数量
  • getPoolSize():线程池当前的线程数量
  • getActiveCount();当前线程池中正在执行任务的线程数量

线程池工具类的参数介绍

在介绍线程池原理的时候首先看一下这四个工具类源码。 

           其实这四个工具类都是实例化ThreadPoolExecutor这个类。进入ThreadPoolExecutor里查看这个构造函数。ThreadPoolExecutor构造函数有4种。这里介绍参数最多的。

public ThreadPoolExecutor(int corePoolSize,   //核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//表示线程没有任务执行时最多保持多久时间会终止。
                          TimeUnit unit,     //参数keepAliveTime的时间单位,有7种取值
                          BlockingQueue<Runnable> workQueue, //阻塞队列,存储等待执行的任务,很重要,会对线程池运行过程产生重大影响
                          ThreadFactory threadFactory,  //线程工厂,用来创建线程
                          RejectedExecutionHandler handler //当拒绝处理任务时的策略
)
拒绝策略有四种
    1、AbortPolicy 默认 处理程序遭到拒绝将抛出运行时 RejectedExecutionException 
    2、CallerRunsPolicy  用调用者所在的线程调用任务
    3、DiscardOldestPolicy 如果执行程序没有关闭,阻塞队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
    4、DiscardPolicy  不能执行的任务将被删除,只不过不抛出异常。

    如果运行的线程数少于corePoolSize的时候,直接创建新线程处理任务。
    如果运行的线程数大于等于corePoolSize,小于maximumPoolSize的时候,只有当workQueue满的时候才创建新的线程去处理任务。
    如果设置的corePoolSize和maximumPoolSize相同的话,那么创建线程池大小是固定的。这时候如果有请求,workQueue还没满的时候,就把请求放入workQueue中,等待有空闲的线程从这里面提取。

为什么线程池要用阻塞队列而不用非阻塞队列?
           因为线程执行需要时间,当队列满的情况下,遇到新的任务添加不进去,会出现丢任务的情况。用阻塞队列的话,可以阻
塞添加,等线程执行完,有空余线程,会执行阻塞队列里的任务,这样新的任务可以添加进去。

接下来具体介绍运行原理,以及核心线程数与最大线程数的关系。


线程池原理剖析

提交一个任务到线程池中,线程池的处理流程有三步:

1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。

2、线程池判断阻塞队列是否已满,如果阻塞队列没有满,则将新提交的任务存储在这个阻塞队列里。如果阻塞队列满了,则进入下个流程。

3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务(也是放在线程池里)。如果已经满了,则交给饱和策略来处理这个任务。

 


自定义线程池

 如果上图不理解的话,可以结合代码在想着可能会更好理解。根据四个工具类可以得出想要自定义线程池就实例化ThreadPoolExecutor即可,就是根据需要实例化ThreadPoolExecutor,如下图,自定义一个核心线程数为1,最大线程数为2,阻塞队列为ArrayBlockingQueue。

public class Test {
	public static void main(String[] args) {
            // 核心线程数是1,最大线程数是2,阻塞队列是采用ArrayBlockingQueue(有边界的阻塞队列)
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
            //当执行第一个任务时,直接从线程池中取出线程执行
	    threadPoolExecutor .execute(new TaskThread("任务1"));
            //当执行第二个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务2"));
            //当执行第三个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务3"));
            //当执行第四个的时候,因为大于核心线程数,所以放在阻塞队列中
            threadPoolExecutor .execute(new TaskThread("任务4"));
            //当执行第五个的时候,因为大于核心线程数,且阻塞队列已经被占满,这个时候最大线程数还有空闲线程,所以新建一个线程执行该任务
            threadPoolExecutor .execute(new TaskThread("任务5"));
            //当放入第六个线程的时候,因为2个最大线程数和3个阻塞队列全被占用,所以会报错
            threadPoolExecutor .execute(new TaskThread("任务6"));
            //关闭线程池
	    threadPoolExecutor .shutdown();
	}
}
class TaskThread implements Runnable {
	private String taskName;
	public TaskThred(String taskName) {
		this.taskName = taskName;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+taskName);
	}
}

线程池的合理配置

1、CPU密集型:花费了绝大多数时间在计算上

CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。 
           CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。

CPU密集型任务,就需要尽量压榨CPU,参考值可以设为CPU+1

2、I/O密集型:  花费了大多是时间在等待I/O上。

IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。

IO密集型任务,参考值可以设置为2*CPU。

猜你喜欢

转载自blog.csdn.net/VRival/article/details/83823481