【Java线程】“打工人”初识线程池及自定义线程池实战
目录
- 理论
- 原理
- 线程池创建
- 工作流程图
- 拒绝策略
- 参数设置
- 四种线程池
- 实战
理论
聊一下为什么要使用线程池?
程序的运行本质,就是通过使用系统资源(CPU、内存、网络、磁盘等等)来完成信息的处理,比如在JVM中创建一个对象实例需要消耗CPU的和内存资源,如果你的程序需要频繁创建大量的对象,并且这些对象的存活时间短就意味着需要进行频繁销毁,那么很有可能这段代码就成为了性能的瓶颈。总结下来其实就以下几点。
- 复用相同的资源,减少浪费,减少新建和销毁的成本;
- 减少单独管理的成本,统一交由线程池管理;
- 提高系统响应速度,因为线程池中有现成的资源,不用重新去创建;
简单言之,线程池就是将用过的对象保存起来,等下一次需要这个对象的时候,直接从对象池中拿出来重复使用,避免频繁的创建和销毁。在Java中万物皆对象,那么线程也是一个对象,Java线程是对于操作系统线程的封装,创建Java线程也需要消耗操作系统的资源,因此就有了线程池。
原理
线程池创建
首先了解一下线程池创建以及工作原理。线程池创建主要由ThreadPoolExecutor
类完成。
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}
各个参数说明:
corePoolSize
:线程池中核心线程数的数量
maximumPoolSize
:在线程池中允许存在的最大线程数
keepAliveTime
:如果当前线程池中的线程数超过了corePoolSize,那么如果在keepAliveTime时间内都没有新的任务需要处理,那么超过corePoolSize的这部分线程就会被销毁。默认情况下是不会回收core线程的,可以通过设置allowCoreThreadTimeOut改变这一行为。
unit
:时间单位
workQueue
:工作队列,线程池中的当前线程数大于核心线程的话,那么接下来的任务会放入到队列中
threadFactory
:通过工厂模式来生产线程。创建线程都是通过ThreadFactory来实现的,如果没指定的话,默认会使用Executors.defaultThreadFactory(),一般来说,我们会在这里对线程设置名称、异常处理器等。
handler
:如果超过了最大线程数,那么就会执行我们设置的拒绝策略。
工作流程图
线程池工作流程图如下:
1-> 当任务提交时,线程池先检查当前线程数;如果当前线程数小于核心线程数(corePoolSize),则创建线程并执行任务;比如开始提交任务时,线程数为0;
2->当线程任务不断增加时,创建的线程数等于核心线程数(corePoolSize),则新增的任务将会被添加到工作队列中(workQueue),等待核心线程将当前任务执行结束,重新从工作队列中获取任务执行;
3->当任务非常多时,并且达到工作队列的最大容量,但是当前线程数小于最大线程数(maximumPoolSize),线程池会在核心线程的基础上继续创建线程(非核心线程)执行任务;
4->当任务继续增加时,线程池的线程数达到最大线程数;如果任务继续增加,此时线程池则会采取拒绝策略拒绝执行任务,默采用AbortPolicy策略
,抛出异常。
拒绝策略
ThreadPoolExecutor提供了四种拒绝策略:
- AbortPolicy
丢弃任务,并抛出异常信息。必须处理好异常,否则会打断当前执行的流程。
public static class AbortPolicy implements RejectedExecutionHandler {/*** Creates an {@code AbortPolicy}.*/public AbortPolicy() { }/***直接抛异常出去*/public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {throw new RejectedExecutionException("Task " + r.toString() +" rejected from " +e.toString());}
}
- CallerRunsPolicy
当触发拒绝策略时,如果线程池未关闭,则直接使用调用者线程,执行任务.
public static class CallerRunsPolicy implements RejectedExecutionHandler {/*** Creates a {@code CallerRunsPolicy}.*/public CallerRunsPolicy() { }/***将此任务交给调用者直接执行*/public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {r.run();}}
}
- DiscardOldestPolicy
当触发拒绝策略时,如果线程池未关闭,则丢弃阻塞队列中最老的一个任务,并将新任务加入.
public static class DiscardOldestPolicy implements RejectedExecutionHandler {/*** Creates a {@code DiscardOldestPolicy} for the given executor.*/public DiscardOldestPolicy() { }/*** 丢弃最老的任务*/public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {e.getQueue().poll();e.execute(r);}}
}
- DiscardPolicy
从源码中应该能看出来,此拒绝策略是对于当前任务不做任何操作
,简单言之:直接丢弃。
public static class DiscardPolicy implements RejectedExecutionHandler {/*** Creates a {@code DiscardPolicy}.*/public DiscardPolicy() { }/***不执行任何操作*/public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}
}
除了以上四种拒绝策略,Java还支持自定义拒绝策略,我们实现RejectExecutionHandler
接口即可;
参数设置
上面我们了解了线程池的参数、工作流程、拒绝策略,下面我们了解一下如何设置参数能够达到线程池的最大利用率呢?
正确的定制线程池的长度,需要了解当前计算机配置、所需资源的情况以及任务的特性。比如部署的计算机安装了多少个CPU?多少的内存?任务主要执行是IO密集型还是CPU密集型?所执行任务是否需要数据库连接这样的稀缺资源?
- CPU密集型任务:说明包含了大量的运算操作,比如有N个CPU,那么就配置线程池的容量大小为N+1,这样能获得最优的利用率。因为CPU密集型的线程恰好在某时因为发生一个页错误或者因为其他的原因而暂停,刚好有一个额外的线程,可以确保在这种情况下CPU周期不会中断工作。 此外,如果CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
- IO密集任务:说明CPU大部分时间都是在等待IO的阻塞操作,那么此时就可以将线程池的容量大小配置的大一些。此时可以根据一些参数进行计算大概线程池的数量多少合适。一般为2*CPU核心数。
N:CPU的数量
U:目标CPU的使用率,0<=U<=1
W/C:等待时间与计算时间的比率
那么最优的线程池的大小就是=NU(1+W/C)
四种线程池
newCachedThreadPool
创建可缓存无限制数量的线程池。如果线程中没有空闲线程池的话此时再来任务会新建线程,如果超过60秒此线程无用,那么就会将此线程销毁。简单来说就是忙不来的时候无限制创建临时线程,闲下来再回收。
newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newScheduledThreadPool
创建一个固定大小的核心线程。此线程池支持定时以及周期性执行任务的需求。
了解了上面四种线程池后,大家应该会明白阿里巴巴规约中为什么会推荐手动创建线程池。
解释
FixedThreadPool和SingleThreadExecutor:这两个线程池的实现方式,可以看到它设置的工作队列都是LinkedBlockingQueue,该队列是一个链表形式的队列,此队列是没有长度限制的,是一个无界队列
,那么此时如果有大量请求,就有可能造成OOM。
CachedThreadPool和ScheduledThreadPool:这两个线程池的实现方式,可以看到它设置的最大线程数都是Integer.MAX_VALUE
,那么就相当于允许创建的线程数量为Integer.MAX_VALUE
。此时如果有大量请求来的时候也有可能造成OOM。
实战
手动创建一个线程池,并实现多线程执行任务。
public class ThreadPoolTask {private static final Logger LOG = LoggerFactory.getLogger(ThreadPoolTask.class);public static void main(String[] args) {List<String> idList = new LinkedList<>();for (int i =0; i < 20; i ++) {idList.add("i" + i);}//默认20个任务未被执行falseMap<String, Object> taskMap = new LinkedHashMap<>();idList.forEach(e -> taskMap.put(e, Boolean.FALSE));//自定义线程池ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 5,0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(4));//多线程执行任务idList.forEach(id -> {Future<?> taskResult = threadPoolExecutor.submit(new HandlerTask(taskMap, id));try {taskResult.get();} catch (Exception e) {e.printStackTrace();}});//关闭线程池threadPoolExecutor.shutdown();}
}class HandlerTask implements Runnable {private final Map<String, Object> taskMap;private final String id;public HandlerTask(Map<String, Object> taskMap, String id) {this.taskMap = taskMap;this.id = id;}@Overridepublic void run() {System.out.println("start=" + Thread.currentThread().getName());handler(id);System.out.println("end=" + Thread.currentThread().getName());}private void handler(String id) {Object o = taskMap.get(id);//判断是否被执行过if (!Boolean.TRUE.equals(o)) {//省略业务逻辑taskMap.put(id, true);}}
}
【Java线程】“打工人”初识线程池及自定义线程池实战相关推荐
- 让我又爱又恨的Java《打工人的那些事》
让我又爱又恨的Java<打工人的那些事> 概述 我从事Java行业,差不多有6年了. 对于Java,我是又爱又恨.那么,爱从何说起.恨又从何而谈. 下面我会一一道来. 始于ACM 2014 ...
- jdbc 连接池 java_JDBC自定义连接池过程详解
这篇文章主要介绍了JDBC自定义连接池过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 开发中,"获得连接"和" ...
- 数据库连接池之自定义连接池(mysql)
数据库连接池之自定义连接池(mysql) 上一篇博文是"基于mysql的JDBC的增删改查的封装":点击可查看 今天本仙在昨天JDBC封装增删改查的基础上实现自定义的数据库连接池: ...
- Java高并发编程详解系列-线程池原理自定义线程池
之前博客的所有内容是对单个线程的操作,例如有Thread和Runnable的使用以及ThreadGroup等的使用,但是对于在有些场景下我们需要管理很多的线程,而对于这些线程的管理有一个统一的管理工具 ...
- 四种常用线程池及自定义线程池参数详细分析
文章目录 一.什么是线程池 二.常用的更方便的Executors工厂方法 三.自定义线程池 四.缓冲队列BlockingQueue 五. 排队的三种一般策略 六.拒绝策略 一.什么是线程池 线程池(英 ...
- JAVA 多线程 JAVA 如何开发一个自定义线程池
1.多线程设计介绍 每一个线程的启动和结束都是比较消耗时间和占用资源的. 如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢. 为了解决这个问题,引入线程池这种设计思想. ...
- 【Android 异步操作】线程池 ( 线程池使用示例 | 自定义线程池使用流程 | 自定义任务拒绝处理策略 | 完整代码示例 )
文章目录 一.自定义线程池使用流程 二.自定义任务拒绝处理策略 三.完整代码示例 在博客 [Android 异步操作]线程池 ( 线程池简介 | 线程池初始化方法 | 线程池种类 | AsyncTas ...
- 自定义线程池内置线程池的使用 ThreadPoolExecutor和Executorservice 示例与注意事项
文章目录 线程池介绍 自己设计一个线程池 1.设计ThreadPool类: 2.设计工作队列 3.实现自己设计的线程池 用java的ThreadPoolExecutor自定义线程池 自定义线程池-参数 ...
- 线程池ThreadPool,线程池底层ThreadPoolExecutor方法七大参数,拒绝策略,以及实际开发中高并发下用到哪个线程池?
为什么要用线程池 基本的三个线程池的底层就是ThreadPoolExecutor类 ExecutorService threadPool = Executors.newFixedThreadPool( ...
最新文章
- mysql基础(九) 索引和视图
- boost::hana::make_pair用法的测试程序
- hpux oracle9,oracle 9.2.0.8在HP-UX 11.31 下的安装步骤和注意事项
- 工作166:正确eachrt渲染方式
- BZOJ 3357: [Usaco2004]等差数列( dp )
- 按键精灵打卡怎么写_[按键精灵教程]过新手引导的各种姿势
- Java 获得Class的绝对路径方法
- C语言学习笔记---strlen()函数和sizeof()函数
- 6.Linux 高性能服务器编程 --- 高级 I/O 函数
- OpenCV简单应用(一、摄像头拍照)
- 计算机的选材标准,GBT26642-2011无损检测金属材料计算机射线照相检测方法国家标准.pdf...
- 试题 基础练习 Fibonacci数列
- 理解本真的REST架构风格
- STM32CubeMAX 安装 2020年3月26日
- phpfpm怎么连接mysql_配置nginx、mysql、php-fpm的方法
- go语言 func函数
- SpringCloud实用篇01
- UG背景颜色修改和截图
- 元认知是认知的监督体系
- 申请MallBook分账需要准备哪些材料呢?