几个月前,写了一篇《Java并发学习(一):进程和线程》,其中埋了一个坑,说我不会用线程池,毕竟像咱们这种小学生,在学校做的小破项目也用不到线程池这种高大上的东西,所以一直也没学。但是最近不是准备出去打工了嘛,所以线程池还是要学一下的,不然到时候面试官一问,啥也不会大眼瞪小眼,不是很尴尬吗~~~。这不,这两天看了下线程池,搞篇文章总结一下线程池的基本知识👉

初识线程池

我们先来思考两个问题:

在没有线程池之前我们是怎么用线程的呢,是不是得先new一个线程对象,然后调用start方法去开启一个线程,然后执行run方法里的代码,执行完后再由JVM在适当的时机将其销毁,有新的任务需要用到线程就再new一个出来,这样频繁的创建和销毁线程效率不是很低吗?这就相当于我是老板,现在有一堆活,雇个人做一项工作,做完就把他辞掉,然后再雇一个人,再做工作再辞掉,那我为什么不直接雇一个人让他轮流做不同的工作呢?这样我不就省事了嘛。对于线程而言,我可不可以创建若干个个线程然后复用这些线程去执行不同的任务?

线程池所实现的就是线程的复用。我先准备若干个线程放在线程池备用,如果有新任务来了,就从线程池里取出一个线程处理,处理完了再放回池里,这样就提高了线程的利用率,避免了不必要的开销,当新任务来的时候直接取出线程就能用,不用等待线程的创建。所以线程池不仅可以降低资源消耗,还可以提高响应速度。最关键的是,我们用线程池对线程进行统一的管理和分配,就可以提高线程的可管理性,避免了无限制地创建线程导致系统崩溃。

那么线程池长啥样呢?来张生动形象的图来展示一下👇

Java线程池初步解读-LMLPHP

在这张图中,线程池中有六个坑位,代表了最多可以容纳六个线程,核心线程池中有4个线程,这里面的线程是长期工,核心线程池外的两个线程是临时工。线程池外有个任务队列,里面有三个坑位,是用来存放临时任务的。

线程池是怎么处理任务的

当我们向线程池中添加一个新任务的时候,线程池是如何分配线程来处理任务的呢?

Java线程池初步解读-LMLPHP

这张流程图和《Java并发编程的艺术》里面的差不多,图中用红色虚线框起来的是我在其基础上添加的,这是我对线程池处理任务流程的理解。

  • 核心线程池中的线程默认情况下是当有任务提交的时候才开始创建,而且就算空闲的线程足以处理新任务,它仍然会创建新的线程去处理,直到核心线程数达到最大值,如果调用了prestartAllCoreThreads()方法则会事先创建所有核心线程。正常情况下,核心线程池中的线程一但创建了就不会自动被销毁,除非设置了allowCoreThreadTimeOut=true,或者是线程在执行任务的时候报了异常。这也是我上一节将其称为长期工的原因。

  • 如果核心线程池的线程已经满了并且都在执行任务的话,新任务就会暂时被存放在任务队列中,当核心线程池中有空闲的线程后就会从任务队列中去取一个任务处理。

  • 那么核心线程池中的线程都在工作,并且任务队列已经排满了,这时候就会创建新的线程去处理任务,这些线程我将其称为临时工,临时工线程在线程池中但不在核心线程池中。当临时工在指定的时间内没有处理任务就会被“辞退”,也就是被销毁,不像核心线程池中的线程那样不干活也可以长期驻留。

  • 当线程池中的线程都在工作,并且任务队列也满了,那么新的任务就会交给饱和策略去处理。

Java线程池初步解读-LMLPHP

线程池的创建与使用

Java线程池初步解读-LMLPHP

直接new一个ThreadPoolExecutor就可以创建一个线程池。里面有几个参数:

  • corePoolSize:核心线程池中的线程数,作用上一节已经说过了,这里大小设的是4,和文章开头的示意图一样
  • maximumPoolSize:线程池中线程的最大数量,也就是说临时工线程是6-4=2个
  • keepAliveTime:临时工线程的存活时间,超过指定时间就会被销毁
  • unit:时间的单位
  • workQueue:任务队列
  • handler:饱和策略

要执行任务的话就调用execute方法,传入一个Runnable对象,任务逻辑写在run方法里。

现在来验证一下上一节中提到的线程池处理任务的逻辑是不是正确的。

int taskNum = 4;
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 6, 1L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < taskNum; i++) {
    threadPoolExecutor.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"     "+"欢迎关注微信公众号:Robod");
        }
    });
}
Thread.sleep(2000);
System.out.println("当前线程池中线程数为:"+threadPoolExecutor.getPoolSize());
threadPoolExecutor.shutdown();	//关闭线程池

接下来通过调整taskNum的值,来观察线程池的执行情况:

Java线程池初步解读-LMLPHP

第一种情况👆,在任务数不超过核心线程数的情况下,不管是否有空闲的线程,都会去创建新的线程去执行任务。

Java线程池初步解读-LMLPHP

第二种情况👆,任务数超过了核心线程数,但没有超过核心线程数+任务队列容量的时候,会将处理不过来的任务放在任务队列中,然后复用核心线程去处理任务,不会去创建临时工线程。

Java线程池初步解读-LMLPHP

第三种情况👆,当核心线程都被占用并且队列已满的情况下,会创建临时工线程去处理任务,当临时工线程超过指定时间没有处理任务时就会被销毁。

Java线程池初步解读-LMLPHP

第四种情况👆,当线程池线程都在处理任务,并且队伍队列也满了的时候,新任务就会交给饱和策略去处理,默认的是AbortPolicy,也就是直接抛异常。

任务队列

当线程池中的线程处理不过来的时候,就会将任务暂时放入任务队列中,任务队列也有好几种:

  • ArrayBlockingQueue:这个是我上一节中用到的,它是基于数组实现的有界阻塞队列,按照先进先出的顺序对元素进行排列
  • LinkedBlockingDeque:基于链表实现的的阻塞队列,初始化的时候不指定容量的话,默认容量是Integer.MAX_VALUE,相当于无界队列,也是按照先进先出的规则排序元素
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。也就是说,当任务数超过线程池最大线程数时就会执行饱和策略
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

饱和策略

  • AbortPolicy:这个是默认的策略,当线程池中的线程都在工作,并且任务队列已满的时候,再试图进来任务的话execute方法就会抛出异常,记得一定要捕获异常,不然程序就会终止。

    Java线程池初步解读-LMLPHP

    这里将任务数设为20,最终有三个任务没能执行,抛出了异常。

  • CallerRunsPolicy只用调用者所在线程来运行任务,什么意思呢?比如下图,我是在主线程中调用线程池去处理任务的,现在有三个任务线程池没来得及处理,就交给调用者,也就是主线程来执行。

    Java线程池初步解读-LMLPHP

  • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

  • DiscardPolicy:不处理,丢弃掉。

总结

好了,文章到这里就结束了。在本文中,介绍了线程池的组成,然后又介绍了下线程池处理新任务的过程。接着讲了下怎么使用线程池,并将任务队列和饱和策略也简单介绍了一下。感觉写的还有很多瑕疵,但是如果小伙伴们能耐心看完的话,我相信还是能有一定收获的。让我们下期再见!

Java线程池初步解读-LMLPHP

11-01 19:40