有点笨,参考了好几篇大佬们写的文章才整理出来的笔记....

字面意思上解释,线程池就是装有线程的池,我们可以把要执行的多线程交给线程池来处理,和连接池的概念一样,通过维护一定数量的线程池来达到多个线程的复用。

好处

多线程产生的问题

一般我们使用到多线程的编程的时候,需要通过new Thread(xxRunnable).start()创建并开启线程,我们可以使用多线程来达到最优效率(如多线程下载)。

但是,线程不是越多就越好,线程过多,创建和销毁就会消耗系统的资源,也不方便管理。

除此之外,多线程还会造成并发问题,线程并发数量过多,抢占系统资源从而导致阻塞。

线程池优点

我们将线程放入线程池,由线程池对线程进行管理,可以对线程池中缓冲的线程进行复用,这样,就不会经常去创建和销毁线程了,从而省下了系统的资源。

线程池能够有效的控制线程并发的数量,能够解决多线程造成的并发问题。

除此之外,线程池还能够对线程进行一定的管理,如延时执行、定时循环执行的策略等

线程池实现

线程池的实现,主要是通过这个类ThreadPoolExecutor,其的构造参数非常长,我们先大概了解,之后再进行详细的介绍。

  1. public ThreadPoolExecutor(int corePoolSize,
  2. int maximumPoolSize,long keepAliveTime,
  3. TimeUnit unit,BlockingQueue workQueue,
  4. RejectedExecutionHandler handler)
  • corePoolSize:线程池核心线程数量
  • maximumPoolSize:线程池最大线程数量
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
  • unit:存活时间的单位
  • workQueue:存放线程的工作队列
  • handler:超出线程范围和队列容量的任务的处理程序(拒绝策略)

这里大概简单说明一下线程池的运行流程:

当线程被添加到线程池中,如果线程池中的当前的线程数量等于线程池定义的最大核心线程数量(corePoolSize)了,此线程就会别放入线程的工作队列(workQueue)中,等待线程池的调用。

Java提供了一个工具类Excutors,方便我们快速创建线程池,其底层也是调用了ThreadPoolExecutor

不过阿里巴巴Java规范中强制要求我们应该通过ThreadPoolExecutor来创建自己的线程池,使用Excutors容易造成OOM问题。

所以,我们先从Excutors开始学习,之后在对ThreadPoolExecutor进行详细的讲解

Excutors

由于Excutors是工具类,所以下面的介绍的都是其的静态方法,如果是比较线程数目比较少的小项目,可以使用此工具类来创建线程池

PS:把线程提交给线程池中,有两种方法,一种是submit,另外一种则是execute

两者的区别:

  1. execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
  2. submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常

线程池可以接收两种的参数,一个为Runnable对象,另外则是Callable对象

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

主要的几个静态方法:

方法 说明
newFixedThreadPool(int nThreads) 创建固定大小的线程池
newSingleThreadExecutor() 创建只有一个线程的线程池
newCachedThreadPool() 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行
newScheduledThreadPool(int nThreads) 创建一个支持定时、周期性或延时任务的限定线程数目的线程池
newSingleThreadScheduledExecutor() 创建一个支持定时、周期性或延时任务的单个线程的线程池

1.newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,我们可以使用它来达到控制线程顺序执行。

控制进程顺序执行:

  1. Thread thread1 = new Thread(new Runnable() {
  2. @Override
  3. public void run() {
  4. try {
  5. System.out.println("这是线程1");
  6. Thread.sleep(1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. }
  11. });
  12. Thread thread2 = new Thread(new Runnable() {
  13. @Override
  14. public void run() {
  15. try {
  16. System.out.println("这是线程2");
  17. Thread.sleep(1000);
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. });
  23. Thread thread3 = new Thread(new Runnable() {
  24. @Override
  25. public void run() {
  26. try {
  27. System.out.println("这是线程3");
  28. Thread.sleep(1000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. });
  34. //创建线程池对象
  35. ExecutorService executorService = Executors.newSingleThreadExecutor();
  36. //把线程添加到线程池中
  37. executorService.submit(thread1);
  38. executorService.submit(thread2);
  39. executorService.submit(thread3);

之后出现的结果就是按照顺序输出

2.newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()

3.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

代码:

  1. //创建了一个自定义的线程
  2. public class MyThread extends Thread {
  3. private int index;
  4. public MyThread(int index) {
  5. this.index = index;
  6. }
  7. @Override
  8. public void run() {
  9. System.out.println(index+" 当前线程"+Thread.currentThread().getName());
  10. }
  11. }
  12. //创建缓存线程池
  13. ExecutorService executorService = Executors.newCachedThreadPool();
  14. for (int i = 0; i < 10; i++) {
  15. executorService.execute(new MyThread(i));
  16. try {
  17. //这里模拟等待时间,等待线程池复用回收线程
  18. Thread.sleep(1000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }

可以看到结果都是使用的同一个线程

4.newScheduledThreadPool

创建一个定长线程池,支持定时、周期性或延时任务执行

延迟1s后启动线程:

  1. ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
  2. scheduledExecutorService.schedule(new MyThread(1),1, TimeUnit.SECONDS);

ThreadPoolExecutor

构造方法

上面提到的那个构造方法其实只是ThreadPoolExecutor类中的一个,ThreadPoolExecutor类中存在有四种不同的构造方法,主要区别就是参数不同。

  1. //五个参数的构造函数
  2. public ThreadPoolExecutor(int corePoolSize,
  3. int maximumPoolSize,
  4. long keepAliveTime,
  5. TimeUnit unit,
  6. BlockingQueue<Runnable> workQueue)
  7. //六个参数的构造函数-1
  8. public ThreadPoolExecutor(int corePoolSize,
  9. int maximumPoolSize,
  10. long keepAliveTime,
  11. TimeUnit unit,
  12. BlockingQueue<Runnable> workQueue,
  13. ThreadFactory threadFactory)
  14. //六个参数的构造函数-2
  15. public ThreadPoolExecutor(int corePoolSize,
  16. int maximumPoolSize,
  17. long keepAliveTime,
  18. TimeUnit unit,
  19. BlockingQueue<Runnable> workQueue,
  20. RejectedExecutionHandler handler)
  21. //七个参数的构造函数
  22. public ThreadPoolExecutor(int corePoolSize,
  23. int maximumPoolSize,
  24. long keepAliveTime,
  25. TimeUnit unit,
  26. BlockingQueue<Runnable> workQueue,
  27. ThreadFactory threadFactory,
  28. RejectedExecutionHandler handler)

首先,有个概念需要明白,线程池的最大线程数(线程总数,maximumPoolSize)= 核心线程数(corePoolSize)+非核心线程数

  • corePoolSize:线程池核心线程数量
  • maximumPoolSize:线程池最大线程数量
  • keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
  • unit:存活时间的单位
  • workQueue:存放线程的工作队列
  • handler:超出线程范围和队列容量的任务的处理程序(拒绝策略)

核心线程和非核心线程有什么区别呢?

核心线程是永远不会被线程池丢弃回收(即使核心线程没有工作),非核心线程则是超过一定时间(keepAliverTime)则就会被丢弃

workQueue

当所有的核心线程都在工作时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

1.SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现线程数达到了maximumPoolSize而不能新建线程的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

2.LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

3.ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

4.DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

拒绝策略:

拒绝策略 拒绝行为
AbortPolicy 抛出RejectedExecutionException异常(默认)
DiscardPolicy 不处理,丢弃掉
DiscardOldestPolicy 丢弃执行队列中等待最久的一个任务,尝试为新来的任务腾出位置
CallerRunsPolicy 直接由提交任务者执行这个任务

两种方法设置拒绝策略:

  1. //ThreadPoolExecutor对象的setRejectedExecutionHandler方法设置
  2. ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS, queue);
  3. threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
  4. //构造方法进行设置,省略

线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。

如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。

ThreadFactory

一个接口类,用来对线程进行设置,需要实现newThread(Runnable r)方法

官方的文档说明:

newThread此方法一般来初始化线程的优先级(priority),名字(name),守护进程(daemon)或线程组(ThreadGroup)

简单的例子(让某个类实现ThreadFactory接口):

  1. @Override
  2. public Thread newThread(Runnable r) {
  3. Thread thread = new Thread(r);
  4. thread.setDaemon(true);
  5. return thread;
  6. }

线程池获取执行结果

PS:把线程提交给线程池中,有两种方法,一种是submit,另外一种则是execute

两者的区别:

  1. execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
  2. submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常

线程池可以接收两种的参数,一个为Runnable对象,另外则是Callable对象

Callable是JDK1.5时加入的接口,作为Runnable的一种补充,允许有返回值,允许抛出异常。

线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。

获取执行结果的代码可以这样写:

  1. ExecutorService executorService = Executors.newFixedThreadPool(4);
  2. Future<Object> future = executorService.submit(new Callable<Object>() {
  3. @Override
  4. public Object call() throws Exception {
  5. //该异常会在调用Future.get()时传递给调用者
  6. throw new RuntimeException("exception in call~");
  7. }
  8. });
  9. try {
  10. //获得返回结果
  11. Object result = future.get();
  12. } catch (InterruptedException e) {
  13. // interrupt
  14. } catch (ExecutionException e) {
  15. // exception in Callable.call()
  16. e.printStackTrace();
  17. }

线程池运行流程

一个形象的比喻说明线程池的流程:

规定:

  1. 线程池比作成一家公司
  2. 公司的最大员工数为maximumPoolSize
  3. 最大正式员工数为coolPoolSize(核心线程的总数)
  4. 最大员工数(maximumPoolSize) = 最大正式员工(coolPoolSize)和临时工(非核心线程)
  5. 单子(任务)可看做为线程
  6. 队列使用的是ArrayBlockingQueue
  7. 一个员工只能干一个任务

最开始的时候,公司是没有一名员工。之后,公司接到了单子(任务),这个时候,公司才去找员工(创建核心线程并让线程开始执行),这个时候找到的员工就是正式员工了。

公司的声誉越来越好,于是来了更多的单子,公司继续招人,直到正式员工数量达到最大的正式员工的数量(核心线程数量已达到最大)

于是,多出来的单子就暂时地存放在了队列中,都在排队,等待正式员工们把手头的工作做完之后,就从队列中依次取出单子继续工作。

某天,来了一个新单子,但是这个时候队列已经满了,公司为了自己的信誉和声誉着想,不得已只能去找临时工(创建非核心线程)来帮忙开始进行工作(负责新单子)

在此之后,又来了新单子,公司继续去招临时工为新来的单子工作,直到正式工和临时工的数量已经达到了公司最大员工数。

这个时候,公司没有办法了,只能拒绝新来的单子了(拒绝策略)

此时,正式工和临时工都是在加班加点去从队列中取出任务来工作,终于某一天,队列的已经没有单子了,市场发展不好,单子越来越少,临时工很久都不工作了(非核心线程超过了最大存活时间keepAliveTime),公司就把这些临时工解雇了,直到剩下只有正式员工。

PS:如果也想要解雇正式员工(销毁核心线程),可以设置ThreadPoolExecutor对象的的allowCoreThreadTimeOut这个属性为true

个人理解,可能不是很正确,仅供参考!

线程池关闭

方法 说明
shutdown() 不再接受新的任务,之前提交的任务等执行结束再关闭线程池
shutdownNow() 不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。

总结

如果是小的Java程序,可以使用Excutors,如果是服务器程序,则使用ThreadPoolExecutor进行自定义线程池的创建

参考链接:

java中常用线程池的:newCachedThreadPool

Java线程池详解

Java 线程池的认识和使用

Java 线程池全面解析

线程池,这一篇或许就够了

Java线程池的运行原理以及使用详解

Java学习笔记 线程池使用及详解的更多相关文章

  1. Java多线程学习之线程池源码详解

    0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...

  2. java学习笔记 - 线程池(一)

    线程池(Thread Pool):把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销 优点:(面试题)可重复使用已有线程,避免对象创建.消亡和过度切换的性能开 ...

  3. Java自带线程池和队列详解

    Java线程池使用说明 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极其之前的jdk版本中,关于线程池的使用是极其简陋的.在jdk1.5之后这一情况有了很大的改观.Jdk1.5之后 ...

  4. 零拷贝详解 Java NIO学习笔记四(零拷贝详解)

    转 https://blog.csdn.net/u013096088/article/details/79122671 Java NIO学习笔记四(零拷贝详解) 2018年01月21日 20:20:5 ...

  5. IP地址和子网划分学习笔记之《IP地址详解》

    2018-05-03 18:47:37   在学习IP地址和子网划分前,必须对进制计数有一定了解,尤其是二进制和十进制之间的相互转换,对于我们掌握IP地址和子网的划分非常有帮助,可参看如下目录详文. ...

  6. Java基础-DBCP连接池(BasicDataSource类)详解

    Java基础-DBCP连接池(BasicDataSource类)详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 实际开发中“获得连接”或“释放资源”是非常消耗系统资源的两个过程 ...

  7. 线程池底层原理详解与源码分析(补充部分---ScheduledThreadPoolExecutor类分析)

    [1]前言 本篇幅是对 线程池底层原理详解与源码分析  的补充,默认你已经看完了上一篇对ThreadPoolExecutor类有了足够的了解. [2]ScheduledThreadPoolExecut ...

  8. Java线程池带图详解

    线程池作为Java中一个重要的知识点,看了很多文章,在此以Java自带的线程池为例,记录分析一下.本文参考了Java并发编程:线程池的使用.Java线程池---addWorker方法解析.线程池.Th ...

  9. Java:多线程,线程池,ThreadPoolExecutor详解

    1. ThreadPoolExecutor的一个常用的构造方法 ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepA ...

随机推荐

  1. Linux常用命令(1)

      常用命令(1)   1.系统相关命令 su 切换用户 hostname 查看主机名 who 查看登录到系统的用户 whoami 确认自己身份 history 查看运行命令的历史 ifconfig ...

  2. Redis(十一)缓存设计

    一.缓存的收益和成本 左侧为客户端直接调用存储层的架构,右侧为比较典型的缓存层+存储层架构, 缓存加入后带来的收益如下: 加速读写:因为缓存通常都是全内存的(例如Redis.Memcache),而存储 ...

  3. UIAlert

    转自:https://blog.csdn.net/deng0zhaotai/article/details/53887508 通过uialertcontroller实现三种简易弹框 (一)警告类 - ...

  4. swift 手机号、邮箱、网址等正则表达式验证

    看到一个不错的swift的 手机号.邮箱.网址等正则表达式验证,分享给大家. 支持swift3,经过修改后,亲测可用! import Foundation enum Validate { case e ...

  5. 【Leetcode 做题学算法周刊】第二期

    首发于微信公众号<前端成长记>,写于 2019.11.05 背景 本文记录刷题过程中的整个思考过程,以供参考.主要内容涵盖: 题目分析设想 编写代码验证 查阅他人解法 思考总结 目录 20 ...

  6. 安卓—自定义 AlertDialog 的样式

    自定义修改安卓弹出框的样式 效果图: 1.在style.xml下添加 <!-- 自定义弹出样式 --> <style name="MyDialogStyle" p ...

  7. [系列] Go gRPC 调试工具

    目录 概述 写一个 gRPC API grpcui 使用 go-gin-api 系列文章 概述 最近这段时间工作挺忙的,发现已经 3 周没更文了... 感谢你们还在,今天给大家分享一款 gRPC 的调 ...

  8. 深度解密Go语言之 pprof

    目录 什么是 pprof pprof 的作用 pprof 如何使用 runtime/pprof net/http/pprof pprof 进阶 Russ Cox 实战 查找内存泄露 总结 参考资料 相 ...

  9. jq实现多选框及反选

    1 效果图 2 html <div class="main"> <table> <tr> <th><input type=&q ...

  10. 深入理解计算机系统 第八章 异常控制流 part1

    本章主旨 第八章的目的是阐述清楚应用程序是如何与操作系统交互的(之前章节的学习是阐述应用程序是如何与硬件交互的) 异常控制流 异常控制流,即 ECF(exceptional contril flow) ...