一、 使用背景

谈到Java多线程,我们很自然的会想到并发,在编写多线程代码时,我们一般会创建多个线程,如果并发的线程数量很多,而且每个线程都是执行一个时间很短的任务就结束了,这样频繁的进行线程的创建会降低系统的效率,因为频繁创建和销毁线程是需要时间的。

那么有没有一种办法可以使得线程可以复用,就是执行完一个任务之后,线程不被销毁,而是用来执行其他任务呢?答:线程池

二、 线程池核心类:ThreadPoolExecutor

java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。下面我们来看一下ThreadPoolExecutor类的具体实现源码。

在ThreadPoolExecutor类中提供了四个构造方法:

  1. public class ThreadPoolExecutor extends AbstractExecutorService {
  2. .....
  3. public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
  4. BlockingQueue<Runnable> workQueue);
  5. public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
  6. BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
  7. public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
  8. BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
  9. public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
  10. BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
  11. ...
  12. }

下面解释构造函数里面每个参数的意义:

  • corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位。
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

    ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue;
  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略,有以下四种取值:
  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

下面说明一下ThreadPoolExecutor的继承关系:

Executor —> ExecutorService —> AbstractExecutorService —> ThreadPoolExecutor

感兴趣的同学可以自行翻阅API文档查阅了解。

三、线程池原理

下面我们要从以下几个方面讲解线程池的原理:

  • 1.线程池状态
  • 2.任务的执行
  • 3.线程池中的线程初始化
  • 4.任务缓存队列及排队策略
  • 5.任务拒绝策略
  • 6.线程池的关闭
  • 7.线程池容量的动态调整

1.线程池状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

  1. volatile int runState;
  2. static final int RUNNING = 0;
  3. static final int SHUTDOWN = 1;
  4. static final int STOP = 2;
  5. static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

  • 当创建线程池后,初始时,线程池处于RUNNING状态;
  • 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
  • 如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
  • 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

2.任务的执行

在了解任务执行过程之前,我们先看一下ThreadPoolExecutor的类里面一些重要的成员变量:

  1. private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
  2. private final ReentrantLock mainLock = new ReentrantLock(); //线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
  3. private final HashSet<Worker> workers = new HashSet<Worker>(); //用来存放工作集
  4. private volatile long keepAliveTime; //线程存活时间
  5. private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
  6. private volatile int corePoolSize; //核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
  7. private volatile int maximumPoolSize; //线程池最大能容忍的线程数
  8. private volatile int poolSize; //线程池中当前的线程数
  9. private volatile RejectedExecutionHandler handler; //任务拒绝策略
  10. private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
  11. private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
  12. private long completedTaskCount; //用来记录已经执行完毕的任务个数

在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以只需要研究execute()方法的实现原理即可:

  1. public void execute(Runnable command) {
  2. if (command == null)
  3. throw new NullPointerException();
  4. if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
  5. if (runState == RUNNING && workQueue.offer(command)) {
  6. if (runState != RUNNING || poolSize == 0)
  7. ensureQueuedTaskHandled(command);
  8. }
  9. else if (!addIfUnderMaximumPoolSize(command))
  10. reject(command); // is shutdown or saturated
  11. }
  12. }

到这里,大部分朋友应该对任务提交给线程池之后到被执行的整个过程有了一个基本的了解,下面总结一下:

1)首先,要清楚corePoolSize和maximumPoolSize的含义;

2)其次,要知道Worker是用来起到什么作用的;

3)要知道任务提交给线程池之后的处理策略,这里总结一下主要有4点:

  • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
  • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
  • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
  • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

3. 线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。

在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

  • prestartCoreThread():初始化一个核心线程;
  • prestartAllCoreThreads():初始化所有核心线程

下面是这2个方法的实现:

  1. public boolean prestartCoreThread() {
  2. return addIfUnderCorePoolSize(null); //注意传进去的参数是null
  3. }
  4. public int prestartAllCoreThreads() {
  5. int n = 0;
  6. while (addIfUnderCorePoolSize(null))//注意传进去的参数是null
  7. ++n;
  8. return n;
  9. }

注意上面传进去的参数是null,根据第2小节的分析可知如果传进去的参数为null,则最后执行线程会阻塞在getTask方法中的

  1. r = workQueue.take();

即等待任务队列中有任务。

4.任务缓存队列及排队策略

在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。

workQueue的类型为BlockingQueue,通常可以取下面三种类型:

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

5.任务拒绝策略

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

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

6.线程池的关闭

ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

  • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

7.线程池容量的动态调整

ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

  • setCorePoolSize:设置核心池大小
  • setMaximumPoolSize:设置线程池最大能创建的线程数目大小

    当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

四、 合理配置线程池的大小

一般需要根据任务的类型来配置线程池大小:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
  • 如果是IO密集型任务,参考值可以设置为2*NCPU

当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

五、参考资料

  1. http://ifeve.com/java-threadpool/
  2. http://blog.163.com/among_1985/blog/static/275005232012618849266/
  3. http://developer.51cto.com/art/201203/321885.htm
  4. http://blog.csdn.net/java2000_wl/article/details/22097059
  5. http://blog.csdn.net/cutesource/article/details/6061229
  6. http://blog.csdn.net/xieyuooo/article/details/8718741

Java 多线程之线程池的使用的更多相关文章

  1. Java多线程与线程池技术

    一.序言 Java多线程编程线程池被广泛使用,甚至成为了标配. 线程池本质是池化技术的应用,和连接池类似,创建连接与关闭连接属于耗时操作,创建线程与销毁线程也属于重操作,为了提高效率,先提前创建好一批 ...

  2. Java 多线程:线程池

    Java 多线程:线程池 作者:Grey 原文地址: 博客园:Java 多线程:线程池 CSDN:Java 多线程:线程池 工作原理 线程池内部是通过队列结合线程实现的,当我们利用线程池执行任务时: ...

  3. java多线程、线程池及Spring配置线程池详解

    1.java中为什么要使用多线程使用多线程,可以把一些大任务分解成多个小任务来执行,多个小任务之间互不影像,同时进行,这样,充分利用了cpu资源.2.java中简单的实现多线程的方式 继承Thread ...

  4. Java多线程和线程池

    转自:http://blog.csdn.net/u013142781/article/details/51387749 1.为什么要使用线程池 在Java中,如果每个请求到达就创建一个新线程,开销是相 ...

  5. Java多线程之线程池详解

    前言 在认识线程池之前,我们需要使用线程就去创建一个线程,但是我们会发现有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因 ...

  6. JAVA多线程(三) 线程池和锁的深度化

    github演示代码地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-brian-query-servic ...

  7. Java多线程-ThreadPool线程池(三)

    开完一趟车完整的过程是启动.行驶和停车,但老司机都知道,真正费油的不是行驶,而是长时间的怠速.频繁地踩刹车等动作.因为在速度切换的过程中,发送机要多做一些工作,当然就要多费一些油. 而一个Java线程 ...

  8. java 多线程 4 线程池

    系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互.在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池. 与数据库连接池类似 ...

  9. Java多线程之线程池

    现在是多核的时代,面向多核的编程很重要,因此基于java的并发和多线程开发非常重要. 线程池是于队列密切相关的,其中队列保存了所有等待执行的任务.工作者线程的任务很简单:从队列中获取一个任务,执行任务 ...

  10. Java多线程(四) 线程池

    一个优秀的软件不会随意的创建.销毁线程,因为创建和销毁线程需要耗费大量的CPU时间以及需要和内存做出大量的交互.因此JDK5提出了使用线程池,让程序员把更多的精力放在业务逻辑上面,弱化对线程的开闭管理 ...

随机推荐

  1. hexo博客简易搭建教程

    什么是Hexo Hexo 是一个快速.简洁且高效的博客框架.Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页.官网 Hexo安装 安装 在安装Hex ...

  2. os模块中关于文件/目录常用的函数使用方法

    os模块中关于文件/目录常用的函数使用方法 函数名 使用方法 getcwd() 返回当前工作目录 chdir(path) 改变工作目录 listdir(path='.') 列举指定目录中的文件名('. ...

  3. yml 文件操作方法

    文件读取方法示例: import yaml fr = open('yml_file_address', 'r',encoding='utf-8') data = yaml.load(fr) print ...

  4. CentOS常用命令搜集

    centos是32或者64位:getconf LONG_BIT

  5. 老男孩Python全栈开发(92天全)视频教程 自学笔记16

    day16课程内容: 装饰器: def outer(): x=10 def inner(): print(x) return innerouter()() #inner 是局部变量,10闭包:如果在一 ...

  6. nyoj234 吃土豆 01背包

    思路:假设我们先只考虑一行,规则就是取了i处的土豆,每一个土豆有两种选择,拿与不拿,那么i-1和i+1处的土豆都不能再取,那么要求某一行的最大取值就用一次动态规划即可,dp(i)表示前i个土豆能取得的 ...

  7. 算法训练 K好数 数位DP+同余定理

    思路:d(i,j)表示以i开头,长度为j的K好数的个数,转移方程就是 for(int u = 0; u < k; ++u) { int x = abs(i - u); if(x == 1) co ...

  8. git 命令和使用场景总结

    资料地址:https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000    http://w ...

  9. Mysql group by,order by,dinstict优化

    1.order by优化 2.group by优化 3.Dinstinct 优化 1.order by优化 实现方式: 1. 根据索引字段排序,利用索引取出的数据已经是排好序的,直接返回给客户端: 2 ...

  10. 【前端】Vue2全家桶案例《看漫画》之四、漫画页

    转载请注明出处:http://www.cnblogs.com/shamoyuu/p/vue_vux_app_4.html 项目github地址:https://github.com/shamoyuu/ ...