Java线程池原理分析

其实这篇文章在去年就已经写完了,但是因为懒一直没有整理成博客,今天痛定思痛下定决心,也要把它发表出来!
在这里插入图片描述
好吧,事实上电脑里还存了这么多东西没有整理成博客,我真是服了自己这个拖延症了!

废话不多说,进入正题。

1. 线程池设计理念

了解程序的设计理念,才能更好的理解他的设计思路。

1.1 减少创建/切换线程开销

目前JVM采用的线程模型还是1:1的模型,也就是说一条Java线程对应一条OS线程,那么无论是创建,销毁,还是阻塞唤醒线程,其实都涉及到线程上下文的切换。 也就是说无论是线程中存在的局部变量,线程中程序运行的位置指针,都需要被操作系统精心保存起来,虽然说线程相比较进程来说,已经极大的减少了这些资源的占用,但是比如像早期BIO实现服务器模型那种(如下图),一个客户端连接进来就要开启一条线程,会极大的占用系统资源,降低使用效率。

在这里插入图片描述
因此大神们就相想出一个解决方案,与其等待任务进来,不断地创建再销毁线程,不如维护几个常用线程,然后提交任务进来,让这几个常用线程干活,这样就能极大的减少线程创建和切换时的资源开销。 于是这时候服务器就可以这样实现,不用在像以前一样创建成百上千的线程。
在这里插入图片描述

这就是线程池的核心设计理念,核心理念就是为了减少减少创建/切换线程开销。

1.2 线程池模型

在这里插入图片描述
这张图就是Java默认实现线程池的架构图,现在看可能还是让人一头雾水,这里我分模块来介绍。

1.2.1 BlockingQueue

首先要明白什么是BlockingQueue,即阻塞队列,也就是一个能保证读写线程安全的队列容器, 其原理也很简单,就是使用到了生产者消费者模型线程同步机制,保证了多线程去操作容器时,会变并行为串行操作。

在线程池中的作用,就是作为一个任务存储队列,因为线程池中有多条线程同时去队列中获取任务,因此需要调用其同步获取方法take()

1.2.2 corePool

corePool就是核心线程池,线程池初始化时会先创建几条线程,这几条线程就是核心线程池的线程。

核心线程池相当于线程池的长期工人,线程池创建他们就已经存在,直到线程池销毁他们才会销毁。

1.2.3 maxmumPool

线程池有一种冗余机制,就是当核心线程池忙不过来,而且BlockingQueue满了(满了以后就不能再继续向队列里添加任务),这时线程池就会找一些临时工, 这就是corePool之外的线程,当然maxmumPool也会有上限,当超过这个上限线程池就拒绝工作啦!

说明一点,为什么说maxmumPool中的是临时工,因为他们在一定时间内没有工作以后,线程池会解雇他们,即销毁核心线程池以外的线程。

1.2.4 RejectedExecutionHandler

上面说到,超过maxmumPool以后,线程池会拒绝工作,这个就是线程池的拒绝策略,也就是说拒绝工作以后怎么处理再提交上来的任务。

下面是我的一段笔记:

当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

1.2.3 execute

这个是线程池的核心方法,也是线程池的入口方法,相当于一个核心调度器,来决定提交的任务进入哪个流程。后续我们会对这个方法进行源码分析。

2.线程池使用实例

public class TestThreadPool {

    static class MyTask implements Runnable {

        @Override
        public void run() {
            System.out.println("任务运行在线程:" + Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        // 创建一个线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10,20,1000,
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>());
        for (int i = 0; i <100; i++) {
            // 提交任务
            threadPool.execute(new MyTask());
        }
        // 关闭线程池
        threadPool.shutdown();
    }
}

上面是一段线程池的使用实例,我们创建了一个线程池,然后循环提交了100个任务,然后关闭线程池。可以看到整个使用还是比较简单的。

下面是构造方法,这里我们推荐不使用JDK自带的几种线程池,因为自己进行配置会具有更好的可控性(《阿里巴巴编码规范》中的推荐写法)

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {...}

上面几个参数说明:参考上面线程池模型中解释。

  1. corePoolSize:指定核心线程池大小
  2. maximumPoolSize:指定线程池最大容量
  3. keepAliveTime:这个就是指定上面我说的,maximumPool中线程无任务时的保持时间,因为corePool外的线程都是“临时工”,当他们没任务可干时,就会自动销毁,这里的keepAliveTime就是指没有任务多久之后,这些线程会被销毁
  4. unit:时间单元,可以指定为小时,分钟,秒,毫秒等等
  5. workQueue:就是任务队列,当corePool线程都在执行任务时,新提交的任务进入workQueue。

3. 线程池工作流程

在这里插入图片描述
结合线程池架构图,就会很容易的理解线程池的工作流程

  1. 提交任务到线程池,如果此时池中线程数量小于corePoolSize,就直接创建一个核心池线程,执行任务
  2. 如果线程池中线程数量已经达到corePoolSize就直接提交到BlockQueue任务队列中,此时线程池中的线程会不断从任务队列中取出任务执行
  3. 如果任务队列满了,也就是相当于任务提交太快,导致核心池线程工作响应不及时,此时线程池会开始找临时工,会继续新建线程,直到线程池内总数到达maximumPoolSize,当然新建的线程也会不断从任务队列中取任务
  4. 如果线程池达到maximumPoolSize,再提交的任务会进入拒绝策略

下一篇:线程池源码分析


图片素材来源: