Java中的线程池
来源:互联网 发布:无法更改mac地址 编辑:程序博客网 时间:2024/06/07 04:37
线程池是使用场景最多的并发框架,几乎所有需要异步和并发执行任务的程序都可以使用线程池。类似于数据库链接池,使用线程池能为程序带来如下好处:
1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
线程池的实现原理
与数据库连接池类似,线程池也一句话概括:通过将事先创建好的线程存放起来,在需要的时候直接拿过来使用就可以了。但是,为了提高线程池的性能,实际的线程池要比这复杂得多。当接受到一个新任务到线程池的时候,线程池会有如下处理流程:
1.线程池首先判断核心线程池中线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果都在执行任务,也就是没有空闲线程的话就进入下个流程
2.线程池继续判断工作队列是否已经满了。如果工作队列没有满,则把新提交的任务放入该工作队列中,如果工作队列已经满了,则进入下个流程
3.线程池判断线程池中的线程是否都处于工作状态。如果不是,则创建一个新的线程执行提交的任务,如果是,则执行饱和策略。
执行流程:
在Java中实现的线程池的核心类是ThreadPoolExecutor,该类的execute方法的执行流程就是上面的过程。注意三个关键字:核心线程池、工作队列和饱和策略
细化到ThreadPoolExecutor执行execute方法的过程,对上面的过程补充如下:
- 核心线程池对应corePoolSize变量的值,如果运行的线程小于corePoolSize,则创建新的线程执行任务(这个过程需要获取全局锁);
- 如果运行的线程大于corePoolSize,则将任务加入BlockingQueue(对应工作队列,阻塞队列);
- 如果无法加入则创建新的线程执行任务,这个步骤中,如果创建新线程后当前运行的线程数大于maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExcution()方法。
ThreadPoolExecutor为了避免执行新提交的任务获取全局锁,ThreadPoolExecutor在创建后会执行一个预热过程,所谓预热就是让当前运行的线程数大于等于corePoolSize。这样,后面新提交的任务都将直接加入到BlockingQueue。而这个过程是不需要获取全局锁的,自然就能提高线程池的性能。
源码分析根据上面的分析,在查看ThreadPoolExecutor源码来加深理解
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); int c = ctl.get(); //如果当前正在运行的线程数小于corePoolSize,则创建新的线程 //执行当前任务 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果当前运行的线程数大于等于corePoolSize或者线程创建失败 //则把当前任务放入工作队列 if (isRunning(c) && workQueue.offer(command)) { int recheck = ctl.get(); //判断之前是否已经添加过线程执行该任务(因为可能之前) //创建的线程已经死亡了)或者线程池是否已经关闭。如果 //两个答案都是肯定的,那么选择拒绝执行任务 if (! isRunning(recheck) && remove(command)) reject(command); else if (workerCountOf(recheck) == 0) addWorker(null, false); } //如果线程池任务无法加入到工作队列(说明工作队列满了) //创建一个线程执行任务。如果新创建后当前运行的线程数大于 //maximumPoolSize则拒绝执行任务 else if (!addWorker(command, false)) reject(command); }
如果线程池能够创建线程执行任务,那么将调用addWorker方法,将线程池创建的线程封装为Worker,Worker在执行完任务后还会循环获取队列中任务来执行。看看addWorker方法的源码:
private boolean addWorker(Runnable firstTask, boolean core){ //省略部分代码 boolean workerStarted = false; boolean workerAdded = false; Worker w = null; try { //这里就将提交的任务封装成为Worker了 w = new Worker(firstTask); final Thread t = w.thread; if (t != null) { //使用加锁的方式原子添加工作线程 final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { //在获得锁期间再次检查线程池的运行状态:如果 //线程池已经关闭或者任务为空则抛出异常 int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) throw new IllegalThreadStateException(); //加入Worker数组 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { //如果添加成功则启动线程执行任务 t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
之后我们看看执行t.start()后会发生的事,因为Worker本身实现了Runnable,所以start后将调用Worker的run方法,源码如下:
public void run() { runWorker(this); } 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 { beforeExecute(wt, task); Throwable thrown = null; task.run(); afterExecute(task, thrown); } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
以上源码其实就干了一件事:创建的线程在执行完提交的任务后会反复从BlockingQueue中获取任务来执行。
使用线程池
线程池的创建,使用ThreadPoolExecutor来创建:
构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)
参数说明:
corePoolSize: 线程池的基本大小
提交一个任务到线程池时,线程池会创建一个线程来执行任务。即使线程池中有空闲的线程可以执行该任务,线程池也会创建线程。等到需要执行的任务数大于线程池的基本大小时就不会再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程就会提前创建并启动所有的基本线程。
maximumPoolSize:线程池维护线程的最大数量
线程池允许创建的最大线程数。如果队列满了,并且创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。如果使用了无界任务队列,则这个参数就没什么用。
keepAliveTime: 线程池维护线程所允许的空闲时间
线程池的工作线程空闲后,保持的存活时间。如果任务多,可以增大时间提高线程的利用率。
unit: 线程池维护线程所允许的空闲时间的单位
单位有天 小时 分钟 毫秒 微秒 千分之一毫秒 千分之一微妙
workQueue: 线程池所使用的任务队列
用来保存等待执行任务的阻塞队列。可以选择的有:
ArrayBlockingQueue
LinkedBlockingQueue(吞吐量高于ArrayBlockingQueue,静态工厂Executors.newFixedThreadPool()默认使用它)
SynchronousQueue(吞吐量高于LinkedBlockingQueue,静态工厂Executors.newCachedThreadPool()默认使用它)
PriorityBlockingQueue 无界阻塞队列,使用它则会让maximumPoolSize参数无效
handler: 线程池对拒绝任务的处理策略(饱和策略)
当队列和线程池都满了,说明线程池处于饱和状态。那么必须采用一种策略处理提交的新任务,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛异常。在jdk 1.5中主要提供了如下几种策略。
- AbortPolicy 直接抛出异常
- DiscardOldestPolicy 丢弃队列中最近的一个任务,并执行当前任务
- CallerRunsPolicy 只用调用者所在线程来运行任务
- DiscardPolicy 不处理,丢弃掉
当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志,持久化存储不能处理的任务等等。
向线程池提交任务
使用两个方法来为线程池提交任务:execute() submit()
execute方法用于提交不需要返回值的任务,所以无法判断是否被线程池成功。实例代码:
threadspool.execute(new Runnable{@Overridepublic void run(){ doSomething();}});
submit方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过它可以判断执行是否成功
future有个阻塞式的get()方法,通过它可以获取线程的返回值,它还有个重载方法get(long timeout,TimeUnit unit)方法会阻塞一段时间后返回,但这个时候有可能任务没有执行完。
关闭线程池
调用线程池的shutdown或者shutdownNow方法来关闭线程池。它们的原理是遍历线程池的工作线程,然后逐个调用interrupt方法来中断线程,所以没有响应中断的任务永远无法停止。它们两者还是有点区别,shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。
线程池的监控
通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
- largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
*getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。- getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,让我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。
建议使用有界队列
有界队列能增加系统的稳定性和预警能力。尤其是依赖与数据库操作的任务,因为等待数据库返回结果比较耗时,此时建议使用容量大一点的队列,线程数也建议设大,这样才能更好利用CPU。
现在我们编码来联系线程池:
public static void main(String[] str){ //创建一个顺序存储的阻塞队列,并指定大小为10 BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(10); //创建线程池的饱和策略,AbortPolicy抛异常 RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); //创建线程池,线程池基本大小3 最大线程数为5 线程最大空闲时间10分钟 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.MINUTES,blockingQueue,handler); /* *我们给线程池的基本是3,现在我们给线程池提交四个任务,并不断的查询线程数。 */ //使用execution方法提交任务 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("当前线程ID"+Thread.currentThread().getId()); System.out.println("我是线程1号"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //获取活动线程数 System.out.println("启动一个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount()); threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("当前线程ID"+Thread.currentThread().getId()); System.out.println("我是线程2号"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //获取活动线程数 System.out.println("启动二个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount()); threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("当前线程ID"+Thread.currentThread().getId()); System.out.println("我是线程3号"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //获取活动线程数 System.out.println("启动三个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount()); /* * 第四个任务应该是不会被执行的,而被存储在任务队列中。等待其他任务被执行完。 */ //提交四号线程后。此线程应该存储在队列中 threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("当前线程ID"+Thread.currentThread().getId()); System.out.println("我是线程4号"); try { Thread.sleep(1000000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //获取活动线程数 System.out.println("启动四个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount()); }
运行结果:
启动一个线程后当前活动线程数量:1启动二个线程后当前活动线程数量:2当前线程ID11当前线程ID12启动三个线程后当前活动线程数量:3启动四个线程后当前活动线程数量:3当前线程ID10我是线程1号我是线程3号我是线程2号
可以看到。在任务队列没有满的情况下,线程池只会创建基本数量的线程3个。对于这三个线程没有空闲的时候。第四个线程则会被存储到任务队列,等待空闲线程。
现在我们对上述代码做如下修改:
将任务队列容量设置为1
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(1);
对线程池再提交一个任务,即在任务队列为1情况下提交5个任务
此时的打印为:
启动一个线程后当前活动线程数量:1当前线程ID10我是线程1号启动二个线程后当前活动线程数量:2当前线程ID11我是线程2号启动三个线程后当前活动线程数量:3启动四个线程后当前活动线程数量:3当前线程ID12我是线程3号启动五个线程后当前活动线程数量:4当前线程ID13我是线程5号
可以看的到,当线程池的基本线程数量满了以后,继续提交的线程会保持在任务队列,上代码中四号任务被保存在任务队列等待基本线程来执行它。此时继续提交线程给线程池,由于我们的任务队列已经满了(只初始化了1,并只保存了四号任务),此时对于第5号线程则会创建工作线程来执行它。如果继续不断的添加任务给线程池,但任务数超过参数二的时候,则会执行饱和策略。
参考 《Java并发编程的艺术》
- JAVA中的线程池
- java中的线程池
- Java中的线程池
- java中的线程池
- java中的线程池
- Java中的线程池
- java中的线程池
- java中的线程池
- java 中的线程池
- Java中的线程池
- java中的线程池
- Java中的线程池
- Java中的线程池
- Java中的线程池
- Java中的线程池
- Java中的线程池
- java中的线程池
- JAVA中的线程池
- Struts2笔记--1
- Qt5 模块简介
- 微信小应用资源汇总整理
- 前端自动化管理工具grount
- c
- Java中的线程池
- HCIE知识整理:ospf 的 virtual-link 有哪些用途,在用的时候可能会有什么问题,解决方案是 什么?
- 111. Minimum Depth of Binary Tree
- oracle分区之包含模板的'范围-列表分区'例子
- 哈理工OJ 1394 XianGe的游戏I(水题么?)
- 从上往下打印二叉树
- mac上用终端启动和关闭mysql服务
- Redis 源码阅读笔记1:zmalloc
- 关于canvas 画布