什么是线程池

当我们需要处理某个任务的时候,可以新创建一个线程,让线程去执行任务。线程池的字面意思就是存放线程的池子,当我们需要处理某个任务的时候,可以从线程池里取出一条线程去执行。

为什么需要线程池

首先我们要知道不用线程池,直接创建线程有什么弊端:

  1. 第一个是创建与销毁线程的开销,Java中的线程是映射到操作系统线程上的,频繁地创建和销毁线程会极大地损耗系统的性能。

  2. 线程会占用一定的内存空间,如果我们在同一时间内创建大量的线程执行任务,很有可能出现内存不足的情况。

为了解决这两个问题我们引入线程池的概念,通过复用线程避免重复创建销毁线程带来的开销,同时可以设置最大线程数,避免同时创建大量线程导致内存溢出。

线程池的使用

1.线程池的核心参数

想掌握线程池首先要理解线程池构造函数的参数:

参数名 类型 含义
corePoolSize int 核心线程数
maxPoolSize int 最大线程数
keepAliveTime long 保持存活时间
workQueue BlockingQueue 任务存储队列
threadFactory ThreadFactory 当线程池需要新创建线程的时候,会通过ThreadFactory创建
Handler RejectedExecutionHandler 当线程池无法接受你提交的任务时所采取的拒绝策略

逐个解释这些参数是很难理解的,这里我结合一张线程池处理的流程图进行讲解:

当我们往线程池里提交任务时,如果线程池内的线程数少于corePoolSize,则会直接创建新的线程处理任务;

如果线程池的线程数达到了corePoolSize,并且存储队列没满,则会把任务放到workQueue任务存储队列里;

如果存储队列也满了,但是线程数还没有达到maxPoolSize,这个时候就会继续创建线程执行任务。注意:这个时候线程池内的线程数已经超过了corePoolSize,超过corePoolSize的线程不会一直存活在线程池内,当他们闲下来时并超过keepAliveTime设定的时间后,就会被销毁。

如果线程数已经达到了maxPoolSize,这个时候如果再来任务,线程池就采取Handler所指定的拒绝策略拒绝任务。

2.几种常见的线程池分析

Java为我们提供了几种常用的线程池,通过Executors类可以轻易地获取它们。下面我们通过分析这几种常用线程池的参数,了解这些线程池之间的异同。

  1. newSingleThreadExecutor

从字面上也好理解,这是一个单线程的线程池,它的构造参数如下(创建的时候不需要传参,这里指的是下一层调用线程池构造函数时的传参):

corePoolSize:1

maximumPoolSize(maxPoolSize):1

keepAliveTime:0L

workQueue:LinkedBlockingQueue

其他参数为默认值

大家按照照着上面的流程图模拟提交任务走一遍,就知道为什么这是一个单线程的线程池了。

当初次任务提交的时候,会创建一个线程执行任务;当提交第二个任务的时候,由于corePoolSize值为1,所以任务会放到任务队列中。由于任务队列选择的是LinkedBlockingQueue,底层结构是链表,理论上可以存放几乎无穷多的任务(默认的大小是Integer.MAX_VALUE),所以永远不会触发任务队列已满的条件,也就永远不会继续增加线程,所以该线程池能保持一个单线程的工作状态。

如果这个唯一的线程因为异常结束了,线程池会创建一个新的线程补上。通过阻塞队列,这个线程池能够保证任务是按顺序执行的。

  1. newFixedThreadPool

这是一个固定线程数的线程池,它的构造参数如下:

corePoolSize:n

maximumPoolSize(maxPoolSize):n

keepAliveTime:0L

workQueue:LinkedBlockingQueue

其他参数为默认值

如果理解了 SingleThreadExecutor 是如何限制只有一条线程执行任务的话,那这里固定线程数的原理也是一样的,关键是限定 corePoolSize 和 maxPoolSize 的大小一样,并使用几乎无限容量LinkedBlockingQueue

  1. newCachedThreadPool

可缓存的线程池,我理解的缓存是关于线程的缓存,它的构造参数如下:

corePoolSize:0

maximumPoolSize(maxPoolSize):Integer.MAX_VALUE

keepAliveTime:60L

workQueue:SynchronousQueue

其他参数为默认值

由于corePoolSize为0,所以任务提交到该线程池后会直接到阻塞队列。又由于阻塞队列采用的是SynchronousQueue,这是一种不存储任务的队列,一旦获得任务它就会分发给任务处理线程,所以直接触发流程图中第三个判断框:如果当前线程数小于maxPoolSize就创建线程。由于maxPoolSize设置了一个很大的值,基本上可以无限地创建线程,具体的数量取绝于JVM所能创建的最大线程数。若线程空闲60秒没任务处理便会被线程池回收。

该线程池在处理大量异步短链接任务的时候有较好的性能,在空闲的时候池内是没有线程的,节省了系统的资源。

  1. newScheduledThreadPool

corePoolSize:自定义

maximumPoolSize(maxPoolSize):Integer.MAX_VALUE

keepAliveTime:0

workQueue:DelayedWorkQueue

其他参数为默认值

由于maxPoolSize设置为Integer.MAX_VALUE,该线程池可以无限创建线程,由于阻塞队列选择了DelayedWorkQueue,所以可以周期性地执行任务。

  1. newWorkStealingPool

这个是JDK1.8新加入的线程池,底层使用的是ForkJoinPool。如果使用默认参数创建的话,该线程池能够创建足够多的线程以达到和系统相匹配的并行处理能力。每个线程都有自己的工作队列,如果当前线程工作完了,它会到别的工作队列中“窃取”任务执行,充分地利用了CPU的多核能力。

阿里巴巴关于创建线程池的约规

下面这段话搬运自阿里巴巴Java开发手册,相信大家看完上面的参数解释以及各种线程池的异同后,就不难理解这段约规了:

(六)并发处理

4. 【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。

说明:Executors返回线程池的弊端如下:

1) FixedThreadPool和SingleThreadPool:

允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。

2) CacheThreadPool和ScheduledThreadPool:

允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM。

3. 线程池的数量设置为多少比较合适?

这个问题是没有固定答案的,我们可以先通过业界权威给出的公式计算线程池的数量,然后通过压测进一步确认具体的数量。

业界给出的指导公式是:

  1. 若任务是CPU密集型任务(比如说加密,计算哈希等),线程数可以设置为CPU核心数的1-2倍左右。

  2. 若任务是耗时IO型任务(比如说读写数据库,文件,网络等),线程数的公式为:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均处理时间)

这两种不同设计都遵循着尽力压榨CPU性能的原则。

4. 线程池的五种状态

线程池的五种状态都写在了ThreadPoolExecutor类中了,它们分别是:

  1. RUNNING:接受新任务,并处理新任务
  2. SHUTDOWN:不接受新任务,但是会处理队列中的任务
  3. STOP:不接受新任务,不处理队列中的任务,中断正在处理的任务
  4. TIDYING:所有任务已经结束,workerCount为零,这时线程会转到TIDYING状态,并将运行terminated()钩子方法
  5. TERMINATED:terminated()运行完成

5. 线程池运行的原理

我们先回顾一下如何新创建一个线程处理任务,看懂了再看线程池的原理就简单了:

//首先把我们要放在线程里运行的代码在Runnable接口实现类的run方法中封装好

class MyTask implements Runnable {

    @Override
    public void run() {
        System.out.println("处理任务 + 1");
    }
}

//然后创建一个线程,把该Runnable接口实现类作为构造参数传给线程

public class Basic {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start();
    }
}
//最后调用线程的start方法运行,实际上调用的是Runnable的run方法

在上面的代码中,实现了Runnable接口的实例传入到线程类中,成为了线程对象的一个成员变量,线程运行的时候会调用该实例的run方法。

可以看到如果新创建一个线程来执行任务,任务会和线程耦合在一起。而线程池的关键原理在于它添加了一个阻塞队列,把任务和线程解耦了

在线程池中,有一个worker的概念,这个概念解释起来有点困难,你可以直接理解为worker就是一个线程工人,它手上拿着任务,当调用线程池的runWorker()方法时,线程就会处理一个任务,详细见下面代码

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {//会到阻塞队列中获取任务
            w.lock();
            //...
            try {
                //执行任务
            } finally {
                //...
                w.unlock();
            }
        }
        //...
    } finally {
        //...
    }
}

从代码中可以看到线程池的关键代码就是一个while循环,在while循环中会不断地向阻塞队列中获取任务,获取到了任务就执行。


参考:

  1. 慕课网《玩转Java并发工具,精通JUC,成为并发多面手》课程
  2. https://www.oschina.net/question/565065_86540
  3. https://www.cnblogs.com/dolphin0520/p/3932921.html
  4. https://www.cnblogs.com/ok-wolf/p/7761755.html

从使用到原理,探究Java线程池的更多相关文章

  1. 从使用到原理学习Java线程池

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...

  2. 【转载】从使用到原理学习Java线程池

    线程池的技术背景 在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源.在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收. 所 ...

  3. Java线程池原理解读

    引言 引用自<阿里巴巴JAVA开发手册> [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销 ...

  4. Java线程池的原理及几类线程池的介绍

    刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...

  5. Java线程池使用和分析(二) - execute()原理

    相关文章目录: Java线程池使用和分析(一) Java线程池使用和分析(二) - execute()原理 execute()是 java.util.concurrent.Executor接口中唯一的 ...

  6. Java线程池ThreadPoolExecutor使用和分析(三) - 终止线程池原理

    相关文章目录: Java线程池ThreadPoolExecutor使用和分析(一) Java线程池ThreadPoolExecutor使用和分析(二) - execute()原理 Java线程池Thr ...

  7. 这么说吧,java线程池的实现原理其实很简单

    好处 : 线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配.调优和监控,有以下好处: 1.降低资源消耗: 2.提高响应速度: 3.提高线 ...

  8. Java 线程池原理分析

    1.简介 线程池可以简单看做是一组线程的集合,通过使用线程池,我们可以方便的复用线程,避免了频繁创建和销毁线程所带来的开销.在应用上,线程池可应用在后端相关服务中.比如 Web 服务器,数据库服务器等 ...

  9. java线程池原理

    在什么情况下使用线程池?     1.单个任务处理的时间比较短     2.将需处理的任务的数量大     使用线程池的好处:     1.减少在创建和销毁线程上所花的时间以及系统资源的开销     ...

随机推荐

  1. win10 64位 安装JDK1.8

    win10 64位 jdk1.8 配置Java环境,是否安装JRE. 一.安装得有JRE JDK和JRE分别安装再不同的文件夹下 新建:JAVA_HOME 值:JDK的安装路径 新建:CLASSPAT ...

  2. abp框架运行——前后端分离(基于VUE)

    目录 1.介绍abp 2.abp如何工作 3.运行Domo 3.1官网点击 创建Demo 3.2 配置NetCore,选择Vue 3.3 输入系统名称验证码 4.官方手册文档 5.VUE项目 6. S ...

  3. ZYNQ自定义AXI总线IP应用——PWM实现呼吸灯效果

    一.前言 在实时性要求较高的场合中,CPU软件执行的方式显然不能满足需求,这时需要硬件逻辑实现部分功能.要想使自定义IP核被CPU访问,就必须带有总线接口.ZYNQ采用AXI BUS实现PS和PL之间 ...

  4. YAML语法使用,JSR303数据校验

    YAML YAML是 "YAML Ain't a Markup Language" (YAML不是一种置标语言)的递归缩写 # yaml配置 server: prot: YAML语 ...

  5. Codeforces Round #292 (Div. 2) C. Drazil and Factorial 515C

    C. Drazil and Factorial time limit per test 2 seconds memory limit per test 256 megabytes input stan ...

  6. 如何开发和发布一个Vue插件

    前言 Vue 项目开发过程中,经常用到插件,比如原生插件 vue-router.vuex,还有 element-ui 提供的 notify.message 等等.这些插件让我们的开发变得更简单更高效. ...

  7. 分享一个快速审查js操作Dom的css

    第一步 打开开发者工具第二步 打开 Sources 面板第三步 执行用户操作让对象可见(例如鼠标悬停)第四步 在元素可见的时候按下 F8(与“暂停脚本执行”按钮相同)第五步 点击开发者工具左上角的“选 ...

  8. springmvc 的@ResponseBody 如何使用JSONP?

    JSONP解释 在解释JSONP之前,我们需要了解下”同源策略“这个概念,这对理解跨域有帮助.基于安全的原因,浏览器是存在同源策略机制的,同源策略阻止从一个源加载的文档或脚本获取或设置另一个源加载额文 ...

  9. ARC中__bridge, __bridge__transfer, __bridge_retained 关系

    总结于 IOS Tuturial 中 ARC两章,详细在dropbox pdf 文档. Toll-Free Bridging 当你在 Objective-C 和 Core Foundation 对象之 ...

  10. 逐浪CMS全面升级为.NET Core 3.0性能大提升

    微软发布了.Net Core 3.0正式版,逐浪CMS也在第一时间做了全面升级,并做了一系列的优化和调整,性能大幅提升,并解决了一些历史遗留问题,添加了一些新的功能.如后台固有小程序功能做了升级,功能 ...