concurrent包分析之Executor框架
- 线程生命周期的开销:线程比较少的情况使用new Thread(task)无多大影响,但是如果涉及到线程比较多的情况,应用的性能就会受到影响,如果jdbc创建连接一样,new Thead创建线程也会耗资源、耗时间的。
- 资源的消耗量:活动线程会消耗系统性能,如果运行的线程数量多余可用的处理器数,那么就会有大量空闲的线程占用内存,会给垃圾收集器带来压力,如果有cpu资源竞争,还会有其他性能开销。
- 限定创建线程的数目:如果不设定创建线程的数量,一个任务一个线程无限创建线程,高负载情况下就有可能造成OutOfMemoryError错误。所以像tomcat这种servlet容器的线程池都设置了最大线程数量的。
Executor框架组成
Eexecutor接口:包含Eexecutor、ExecutorService、ScheduledExecutorService
ThreadPool线程池:包含ThreadPoolExecutor、ScheduledThreadPoolExecutor
Fork/Join框架:JDK1.7新增
类之间的关系如下:
Executor框架将线程的创建与执行解耦,可以异步调用,让任务相互独立,用阻塞队列管理任务,直接在当前线程中消费队列,可以减少线程之间进行资源竞争,也可以减少线程的创建和系统的开销,要廉价多了。这种设计就是经典并发模式Active Object Models(也称Actor Models)的实现,如下图:
可以参考execute方法,就按照上面的模式来的。另外Executors工厂类创建了不同的连接池,为任务的执行分配了不同执行策略。
线程池
concurrent包里面主要包含ThreadPoolExecutor、ScheduledThreadPoolExecutor两种线程池,Executors类提供了很多创建线程池的方法
,newFixedThreadPool创建定长的线程池、newWorkStealingPool创建ForkJoinPool(jdk1.8新增)、newCachedThreadPool创建缓存线程池(可以回收空闲的线程)、newSingleThreadExecutor创建当个线程池(保证FIFOLIFO优先级),newScheduledThreadPool创建定时器线程池,如果有特殊处理的,也可以根据自己的需求来创建连接池,如tomcat也是基于ThreadPoolExecutor实现了自己的连接池。
ThreadPoolExecutor
public (int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
从ThreadPoolExecutor最简单的构造函数可以看出,线程池总是依赖阻塞队列而工作的,newFixedThreadPool与newSingleThreadExecutor使用的是LinkedBlockingQueue无界队列,newCachedThreadPool使用SynchronousQueue,ThreadPoolExecutor还定义了一个Worker执行任务线程,除此之外,还有个非常重要的变量ctl(线程池控制状态)由执行器状态和工作线程的数量组成,在控制执行的时候都是围绕这个变量来判断。
处理任务
参考前面的Active Object Models图,可以将线程池执行任务主要分为3个步骤:
1、如果任务少于线程池大小时,就作为firstTask分别创建工作线程执行任务
2、如果第n个任务超出了线程池大小,就加入到阻塞队列,并从新检测执行器状态状态以及工作线程数量(有可能线程发生RuntimeException挂掉,最后可工作线程数量变为0),如果工作线程挂完了就重新启动一个线程(解决线程泄露问题)。阻塞队列的任务执行就在第一步所创建的线程执行,参考代码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
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();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
大专栏 concurrent包分析之Executor框架 Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
3、如果任务入队列失败了,就重新开启一个线程去执行,如果还是失败了,就可以确定是执行器关闭了或者线程池已经达到饱和状态了。
线程池原理
从上面的执行步骤来就可以看出线程池依赖于阻塞队列,然后基于生产者消费者模式实现的,execute()方法一直添加任务(生产者),当任务数量超出线程池的最大长度就添加到阻塞队列等待排队执行,而第一次创建的所有工作线程(最大数量为线程池的最大长度)就会一直判断线程池里面是否有任务执行,如果有就执行任务(消费者)。这样做就可以重用线程了,不用每次去创建线程,性能肯定比一个任务一个线程好多了。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor由ScheduledFutureTask、DelayedWorkQueue组成,实现任务执行还是用的父类ThreadPoolExecutor的工作线程。jdk1.5之前使用定时任务都用的Timer,但是与ScheduledThreadPoolExecutor有着明显区别:
- Timer使用的System.currentTimeMillis()毫秒来控制时间,ScheduledThreadPoolExecutor使用System.nanoTime()纳秒控制更加精准,并且可以使用TimeUnit进行时间的跨单元转换。
- Timer只有单个工作线程,ScheduledThreadPoolExecutor可以配置多个工作线程。
- 如果工资线程发生异常,Timer会造成线程泄露没有重启的线程,ScheduledThreadPoolExecutor会一直检测工作线程的数量,如果没有工作线程了,会一直添加数量小于线程池的工作线程(使用父类ThreadPoolExecutor的addaddWorker方法)
从以上比较可以看出,ScheduledThreadPoolExecutor就是取代Timer的。
还是从一个简单的例子看ScheduledThreadPoolExecutor是如何工作的。addWork参考领导/跟随者模式
@Test
public void testScheduleAtFixedRate() throws Exception {
ScheduledThreadPoolExecutor p = new ScheduledThreadPoolExecutor(1);
CountDownLatch latch = new CountDownLatch(3);
p.scheduleAtFixedRate(new Task(latch), 0, 1000, TimeUnit.MILLISECONDS);
latch.await();
}
线程池都是基于生产者消费者模式的,所以ScheduledThreadPoolExecutor也不例外,执行任务的时候会一直像队列里面添加任务:
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
//添加到队列,让工作线程去消费
super.getQueue().add(task);
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
//启动线程
ensurePrestart();
}
}
ScheduledThreadPoolExecutor第一次执行用的父类ThreadPoolExecutor的addWorker方法添加工作线程并启动它,然后每个工作线程会检查队列里面是否有消费的线程,参考ThreadPoolExecutor的runWorker方法,与ThreadPoolExecutor有点不同的是,ScheduledThreadPoolExecutor使用的是延时阻塞队列DelayedWorkQueue。每次消费就进行take操作:
public RunnableScheduledFuture<?> take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture<?> first = queue[0];
if (first == null)
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
//返回队列中的执行任务之前,要先执行finally模块中的唤醒操作
return finishPoll(first);
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
//领导线程初始化为null,当前线程为线程池中的线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//当前线程等待延迟时间到期
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}
参考资料:
concurrent包分析之Executor框架的更多相关文章
- jdk8中java.util.concurrent包分析
并发框架分类 1. Executor相关类 Interfaces. Executor is a simple standardized interface for defining custom th ...
- Google-Guava Concurrent包里的Service框架浅析
原文地址 译文地址 译者:何一昕 校对:方腾飞 概述 Guava包里的Service接口用于封装一个服务对象的运行状态.包括start和stop等方法.例如web服务器,RPC服务器.计时器等可以实 ...
- java.util.concurrent包学习笔记(一)Executor框架
类图: 其实从类图我们能发现concurrent包(除去java.util.concurrent.atomic 和 java.util.concurrent.locks)中的内容并没有特别多,大概分为 ...
- JDK源码分析之concurrent包(一) -- Executor架构
Java5新出的concurrent包中的API,是一些并发编程中实用的的工具类.在高并发场景下的使用非常广泛.笔者在这做了一个针对concurrent包中部分常用类的源码分析系列.本系列针对的读者是 ...
- JAVA基础之——JDK包分析concurrent
concurrent在哪儿:jdk\jre\lib\rt.jar package java.util.concurrent; 本文从特性.分类.扩展方面一一道来. 1 特性 包中包含大量有用的构建块, ...
- 并发工具箱 concurrent包的原理分析以及使用
1.java.util.concurrent 包下的类分类图 locks部分:显式锁(互斥锁和速写锁)相关: atomic部分:原子变量类相关,是构建非阻塞算法的基础: executor部分:线程池相 ...
- Java:concurrent包下面的Map接口框架图(ConcurrentMap接口、ConcurrentHashMap实现类)
Java集合大致可分为Set.List和Map三种体系,其中Set代表无序.不可重复的集合:List代表有序.重复的集合:而Map则代表具有映射关系的集合.Java 5之后,增加了Queue体系集合, ...
- Java:concurrent包下面的Collection接口框架图( CopyOnWriteArraySet, CopyOnWriteArrayList,ConcurrentLinkedQueue,BlockingQueue)
Java集合大致可分为Set.List和Map三种体系,其中Set代表无序.不可重复的集合:List代表有序.重复的集合:而Map则代表具有映射关系的集合.Java 5之后,增加了Queue体系集合, ...
- JDK源码分析之concurrent包(四) -- CyclicBarrier与CountDownLatch
上一篇我们主要通过ExecutorCompletionService与FutureTask类的源码,对Future模型体系的原理做了了解,本篇开始解读concurrent包中的工具类的源码.首先来看两 ...
随机推荐
- \_\_setitem\_\_和\_\_getitem和\_\_delitem__
目录 __setitem__和__getitem和__delitem__ 一.__setitem__ 二.__getitem__ 三.__delitem__与__delattr__ 四.总结 __se ...
- Python笔记_第二篇_面向过程_第二部分_4.常用模块的简单使用_import语句的解释
1. import语句.from...import语句.from...import*语句 解释:注意一定要在体同一级目录下 1.1 引入模块 格式:import module[,module2,... ...
- Python笔记_第一篇_面向过程_第一部分_9.Ubuntu基础操作
第一部分 Ubuntu简介 Ubuntu(乌班图)是一个机遇Debian的以桌面应用为主的Linux操作系统,据说其名称来自非洲南部祖鲁语或科萨语的“Ubuntu”一词,意思是“人性”.“我的存在 ...
- rsync+nfs+sersync实战案例
回顾: 1.rsync 统一备份各个服务器的配置文件或重要文件 系统配置文件 日志文件 系统日志文件 messages.secure.cron 服务日志文件 access_log.access.log ...
- java select单线程 服务器
package com.Select; /** *select单线程 服务器 **/ import java.io.IOException; import java.net.InetSocketAdd ...
- Springboot中RestTemplate -- 用更优雅的方式发HTTP请求
RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率. 我之前的HTTP开发是用ap ...
- 关于Tarjan的一些问题
$Q:$为什么tarjan求强连通分量要记录点是否在栈内,而求双连通分量不用? $A:$在有向图中存在横叉边,而无向图中不存在: $Q:$为什么用子搜索树中的$low[v]$来更新$low[k]$,而 ...
- [概率DP]相逢是温厚
题意 有\(n\)场比赛,他每次等概率地选择一场,选择的比赛可能有没ac过的题,他一定会ac这次比赛中的某一道,并说我好菜啊.如果全ac过了,也会说我好菜啊.求期望说多少次我好菜啊. 注意题目中每场题 ...
- GPIO口的脚本配置之——全志H3script.bin
此脚本的作用之一是配置GPIO的默认状态: 如:功能,内部电阻状态,驱动能力等. 1.但是直接打开script.bin 文件则会出现乱码,那么我们怎么才可以打开并更改该脚本的配置呢? 在路径uboot ...
- set_include_path详细解释(转)
首先我们来看这个全局变量:__FILE__它表示文件的完整路径(当然包括文件名在内)也就是说它根据你文件所在的目录不同,有着不同的值:当然,当它用在包行文件中的时候,它的值是包含的路径: 然后:我们看 ...