大数据培训新三板挂牌机构 股票代码:837906 | EN CN
Java是什么?
Java历史
Java语言特点
C++ VS Java比较
Java工厂设计模式
Java抽象工厂模式
Java单例模式
Java建造者(Builder)模式
Java原型模式
Java适配器模式
Java桥接模式
Java获取网络文件大小
Java套接字到单一的客户端
Java连接套接字
Java URL部分
Java URL连接日期
Java下载网页
Java主机指定IP地址
Java确定本地IP地址
Java检查端口占用
Java查找代理服务器设置
Java创建Socket
Java线程实例
Java检查线程活着
Java如何检查一个线程停止或没有?
Java解决死锁实例
Java如何获取正在运行的线程的优先级?
Java如何监视线程的状态?
Java获取线程名称
Java线程生产者消费者问题
Java如何设置线程的优先级?
Java如何停止线程一会儿?
Java如何暂停线程?
Java获取线程ID
Java如何检查线程的优先级?
Java显示所有正在运行的线程?
Java显示线程状态
Java中断一个线程
Java Applet实例
Java创建Applet
Java使用Applet创建横幅
Java使用Applet显示时钟?
Java在一个Applet创建不同形状
Java如何使用Applet填充形状的颜色?
Java使用Applet跳转到一个链接
Java在Applet创建事件监听器
Java使用Applet显示图像
Java使用Applet在新窗口中打开链接
Java使用Applet播放声音?
Java使用Applet读取文件
Java使用Applet写入文件
Java中Swing应用程序applet
Java简单的图形用户界面-GUI
Java以不同的字体显示文本
Java使用GUI画一条线
Java创建框架-frame
Java使用GUI显示多边形
Java在矩形中显示文本
Java GUI显示不同形状
Java如何绘制GUI实心矩形?
Java创建GUI透明光标
Java检查GUI平滑处理状态
Java在框架中显示颜色
Java GUI显示饼图
Java使用图形用户界面绘制文本
Java编辑表-table
Java 使用prepared语句
Java使用保存点和回滚
Java同时执行数据库多个SQL命令
Java使用行方法
Java使用列方法
Java正则表达式实例
Java将字符串分割
Java搜索重复单词
Java查找出现的单词
Java最后一个词的索引
Java模式匹配
Java删除空格
Java匹配电话号码
Java计数组词
Java搜索词组
Java拆分正则表达式
Java替换第一个出现字符串
Java检查日期格式
Java验证电子邮件地址格式
Java替换所有匹配字符串
Java使每个单词的第一个字母大写
从XML创建SqlSessionFactory实例
不使用XML来创建SqlSessionFactory
从SqlSessionFactory获取SqlSession
映射SQL语句
作用域和生命周期
Mapper XML配置
properties元素
Settings元素
typeAliases 元素
typeHandlers元素
理解CacheLine与写出更好的JAVA
Java核心技术点之动态代理
更好的使用JAVA线程池
理解Java中字符流与字节流的区别
深入分析Java方法反射的实现原理
关于Java面试,你应该准备这些知识点
Java内存模型
2017年你不能错过的Java类库
Leakcanary Square的一款Android/Java内存泄漏检测工具
Java Synchronised机制
Java核心技术点之注解
JVM(8):JVM知识点总览-高级Java工程师面试必备
JVM(3):Java GC算法 垃圾收集器
JVM(1):Java 类的加载机制
解决ActiveMQ中,Java与C++交互中文乱码问题
关于Java Collections的几个常见问题
Java I/O 总结
JVM源码分析之Java对象的创建过程
JVM源码分析之Java类的加载过程
Java GC的那些事(下)
Java GC的那些事(上)
java对象头的HotSpot实现分析
面试的角度诠释Java工程师(一)
面试的角度诠释Java工程师(二)
框架开发之Java注解的妙用
谈谈Java反射机制
Java并发:volatile内存可见性和指令重排
死磕Java并发:Java内存模型之happens-before
死磕Java并发:深入分析volatile的实现原理
死磕Java并发:深入分析synchronized的实现原理
Java 10 可能对 Lambda 表达式进行升级
G1垃圾回收器中的字符串去重(Java 8 Update 20)
Java RESTful框架的性能比较
理解RxJava的线程模型
继续了解Java的纤程库 – Quasar
Java中的纤程库 – Quasar
Java豆瓣电影爬虫——抓取电影详情和电影短评数据
Java集合框架源码剖析:LinkedHashSet 和 LinkedHashMap
Java Lambda表达式初探
Java中的陷阱题
Java 9的这一基本功能,你可能从未听过
关于Java并发编程的总结和思考
几种简单的负载均衡算法及其Java代码实现
JAVA虚拟机关闭钩子(Shutdown Hook)
Java 脚本化编程指南
Java Scripting API 使用示例
Java 8 的 Nashorn 脚本引擎教程
如何开始使用 Java 机器学习
CognitiveJ —— Java 的图像分析库
Java 性能优化的五大技巧
Java 解惑:Comparable 和 Comparator 的区别
Google Java编程风格指南
java NIO详解
Java 异常处理的误区和经验总结
Java语法糖(4):内部类
Java语法糖(3):泛型
Java语法糖(2):自动装箱和自动拆箱
Java消息队列任务的平滑关闭
Java语法糖(1):可变长度参数以及foreach循环原理
2016最流行的Java EE服务器
自己写一个java.lang.reflect.Proxy代理的实现
java 如何在pdf中生成表格
如何防止单例模式被JAVA反射攻击
java虚拟机 jvm 局部变量表实战
聊聊并发-Java中的Copy-On-Write容器
java.lang.Instrument 代理Agent使用
Java开发者需要了解的移动开发编程语言
13个不容错过的Java项目
2016年7款最佳 Java 框架推荐
Java 开发者值得关注的 11 个技术博客
Redmonk发布Java框架流行度调研结果
Java 8开发的4大顶级技巧
GitHub漫游指南:10个值得你关注的Java项目
除了Guava,Java开发者还值得了解的5个谷歌类库
Java中创建对象的5种不同方法
Java性能优化全攻略
奇怪的Java题:为什么1000 == 1000返回为False,而100 == 100会返回为True?
11个最值得Java开发者收藏的网站
Java的常见误区与细节
对Java意义重大的7个性能指标
Java调优经验谈
关于Java并发编程的总结和思考
HDFS Federation设计动机与基本原理
《Effective STL》学习笔记(第三部分)
《Effective STL》学习笔记(第二部分)
《Effective STL》学习笔记(第一部分)
数据结构之位图
Thrift使用指南
Cassandra概要介绍
Cassandra部署与安装
Cassandra客户端
Cassandra数据模型
Cassandra中的各种策略
数据结构之树状数组
数据结构之伸展树
数据结构之后缀数组
数据结构之堆
浅析MRv1与MRv2的API兼容性
Apache Tez最新进展
运行在YARN上的计算框架
从传统操作系统角度理解Hadoop YARN

更好的使用JAVA线程池

于2017-05-10由小牛君创建

分享到:


这篇文章结合Doug Lea大神在JDK1.5提供的JCU包,分别从线程池大小参数的设置、工作线程的创建、空闲线程的回收、阻塞队列的使用、任务拒绝策略、线程池Hook等方面来了解线程池的使用,其中涉及到一些细节包括不同参数、不同队列、不同拒绝策略的选择、产生的影响和行为、为更好的使用线程池奠定知识基础,其中值得注意的部分我用粗体标识。

Doug Lea

ExecutorService基于池化的线程来执行用户提交的任务,通常可以简单的通过Executors提供的工厂方法来创建ThreadPoolExecutor实例。

线程池解决的两个问题:1)线程池通过减少每次做任务的时候产生的性能消耗来优化执行大量的异步任务的时候的系统性能。2)线程池还提供了限制和管理批量任务被执行的时候消耗的资源、线程的方法。另外ThreadPoolExecutor还提供了简单的统计功能,比如当前有多少任务被执行完了。

快速开始

为了使得线程池适合大量不同的应用上下文环境,ThreadPoolExecutor提供了很多可以配置的参数和可被用来扩展的钩子。然而,用户还可以通过使用Executors提供的一些工厂方法来快速创建ThreadPoolExecutor实例。比如:

  1. 使用Executors#newCachedThreadPool可以快速创建一个拥有自动回收线程功能且没有限制的线程池。
  2. 使用Executors#newFixedThreadPool可以用来创建一个固定线程大小的线程池。
  3. 使用Executors#newSingleThreadExecutor可以用来创建一个单线程的执行器。

如果上面的方法创建的实例不能满足我们的需求,我们可以自己通过参数来配置,实例化一个实例。

关于线程数大小参数设置需要知道的

ThreadPoolExecutor会根据corePoolSize和maximumPoolSize来动态调整线程池的大小:poolSize。

当任务通过executor提交给线程池的时候,我们需要知道下面几个点:

  1. 如果这个时候当前池子中的工作线程数小于corePoolSize,则新创建一个新的工作线程来执行这个任务,不管工作线程集合中有没有线程是处于空闲状态。
  2. 如果池子中有比corePoolSize大的但是比maximumPoolSize小的工作线程,任务会首先被尝试着放入队列,这里有两种情况需要单独说一下:a、如果任务被成功的放入队列,则看看是否需要开启新的线程来执行任务,只有当当前工作线程数为0的时候才会创建新的线程,因为之前的线程有可能因为都处于空闲状态或因为工作结束而被移除。

    b、如果放入队列失败,则才会去创建新的工作线程。

  3. 如果corePoolSize和maximumPoolSize相同,则线程池的大小是固定的。
  4. 通过将maximumPoolSize设置为无限大,我们可以得到一个无上限的线程池。
  5. 除了通过构造参数设置这几个线程池参数之外我们还可以在运行时设置。

核心线程WarmUp

默认情况下,核心工作线程值在初始的时候被创建,当新任务来到的时候被启动,但是我们可以通过重写prestartCoreThread或prestartCoreThreads方法来改变这种行为。通常场景我们可以在应用启动的时候来WarmUp核心线程,从而达到任务过来能够立马执行的结果,使得初始任务处理的时间得到一定优化。

定制工作线程的创建

新的线程是通过ThreadFactory来创建的,如果没有指定,默认的Executors#defaultThreadFactory将被使用,这个时候创建的线程将都属于同一个线程组,拥有同样的优先级和daemon状态。扩展配置ThreadFactory,我们可以配置线程的名字、线程组合daemon状态。如果调用ThreadFactory#createThread的时候失败,将返回null,executor将不会执行任何任务。

空闲线程回收

如果当前池子中的工作线程数大于corePoolSize,如果超过这个数字的线程处于空闲的时间大于keepAliveTime,则这些线程将会被终止,这是一种减少不必要资源消耗的策略。这个参数可以在运行时被改变,我们同样可以将这种策略应用给核心线程,我们可以通过调用allowCoreThreadTimeout来实现。

选择合适的阻塞队列

所有的阻塞队列都可以被用来存放任务,但是使用不同的队列针对corePoolSize会表现不同的行为:

当池中工作线程数小于corePoolSize的时候,每次来任务的时候都会创建一个新的工作线程。

当池中工作线程数大于等于corePoolSize的时候,每次任务来的时候都会首先尝试将线程放入队列,而不是直接去创建线程。

如果放入队列失败,且当先池中线程数小于maximumPoolSize的时候,则会创建一个工作线程。

下面主要是不同队列策略表现:

直接递交:一种比较好的默认选择是使用SynchronousQueue,这种策略会将提交的任务直接传送给工作线程,而不持有。如果当前没有工作线程来处理,即任务放入队列失败,则根据线程池的实现,会引发新的工作线程创建,因此新提交的任务会被处理。这种策略在当提交的一批任务之间有依赖关系的时候避免了锁竞争消耗。值得一提的是,这种策略最好是配合unbounded线程数来使用,从而避免任务被拒绝。同时我们必须要考虑到一种场景,当任务到来的速度大于任务处理的速度,将会引起无限制的线程数不断的增加。

无界队列:使用无界队列如LinkedBlockingQueue没有指定最大容量的时候,将会引起当核心线程都在忙的时候,新的任务被放在队列上,因此,永远不会有大于corePoolSize的线程被创建,因此maximumPoolSize参数将失效。这种策略比较适合所有的任务都不相互依赖,独立执行。举个例子,如网页服务器中,每个线程独立处理请求。但是当任务处理速度小于任务进入速度的时候会引起队列的无限膨胀。

有界队列:有界队列如ArrayBlockingQueue帮助限制资源的消耗,但是不容易控制。队列长度和maximumPoolSize这两个值会相互影响,使用大的队列和小maximumPoolSize会减少CPU的使用、操作系统资源、上下文切换的消耗,但是会降低吞吐量,如果任务被频繁的阻塞如IO线程,系统其实可以调度更多的线程。使用小的队列通常需要大maximumPoolSize,从而使得CPU更忙一些,但是又会增加降低吞吐量的线程调度的消耗。总结一下是IO密集型可以考虑多些线程来平衡CPU的使用,CPU密集型可以考虑少些线程减少线程调度的消耗。

选择适合的拒绝策略

当新的任务到来的而线程池被关闭的时候,或线程数和队列已经达到上限的时候,我们需要去做一个决定,怎么拒绝这些任务。下面介绍一下常用的策略:

ThreadPoolExecutor#AbortPolicy:这个策略直接抛出RejectedExecutionException异常。

ThreadPoolExecutor#CallerRunsPolicy:这个策略将会使用Caller线程来执行这个任务,这是一种feedback策略,可以降低任务提交的速度。

ThreadPoolExecutor#DiscardPolicy:这个策略将会直接丢弃任务。

ThreadPoolExecutor#DiscardOldestPolicy:这个策略将会把任务队列头部的任务丢弃,然后重新尝试执行,如果还是失败则继续实施策略。

除了上面的几种策略,我们也可以通过实现RejectedExecutionHandler来实现自己的策略。

利用Hook嵌入你的行为

ThreadPoolExecutor提供了protected类型可以被覆盖的钩子方法,允许用户在任务执行之前会执行之后做一些事情。我们可以通过它来实现比如初始化ThreadLocal、收集统计信息、如记录日志等操作。这类Hook如beforeExecute和afterExecute。另外还有一个Hook可以用来在任务被执行完的时候让用户插入逻辑,如rerminated。

如果hook方法执行失败,则内部的工作线程的执行将会失败或被中断。

可访问的队列

getQueue方法可以用来访问queue队列以进行一些统计或者debug工作,我们不建议用作其他用途。同时remove方法和purge方法可以用来将任务从队列中移除。

关闭线程池

当线程池不在被引用并且工作线程数为0的时候,线程池将被终止。我们也可以调用shutdown来手动终止线程池。如果我们忘记调用shutdown,为了让线程资源被释放,我们还可以使用keepAliveTime和allowCoreThreadTimeOut来达到目的。

写在最后

JAVA本身提供的API已经可以让我们快速的进行基于线程池的多线程开发,但是我们必须要为我们写的代码负责,每一个参数的设置和策略的选择跟不同应用场景有绝对的关系。然而对于不同参数和不同策略的选择并不是一件容易的事情,我们必须要先回答一些基础问题:每创建一个线程,操作系统为我们做了哪些事情,这个线程的操作系统资源消耗主要在哪部分?假如我的应用场景是IO密集型的,那么我需要更多的线程还是更少的线程?假如我们的CPU操作和IO操作大概各占一半的话我们又需要如何选择?等等一些列问题。我认为、多线程开发是一件很容易的事情也是一件很不容易的事情。:)

参考文档《JDK1.5》 by Dong Lea