Java并发编程笔记4-线程池
我们使用线程的时候就去创建一个线程,但是就会有一个问题:
如果并发的线程数量非常多,而且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会导致大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
线程池正好能解决这样的问题。正如名称所称的那样,线程池管理一个工作者线程的同构池。线程池是与工作队列紧密绑定的。所谓工作队列,其作用是持有所有等待执行的任务。
工作者线程的生活从此轻松起来:它从工作队列中获取下一个任务,执行它,然后回来等待另外一个线程。
这类似于企业应用程序中事务监听器(transaction monitor)的角色:它将课运行事务的数量控制在一个合理的水平中,不会因过渡滥用事务而耗尽有限资源。
线程池中执行任务线程,这方法有很多“每任务每线程”无法笔记的优势。重用存在的线程,而不是创建新的线程,这可以在处理多请求时抵消线程创建,消亡产生的开销。还有一个好处就是,在请求到达时,工作者线程通常已经存在
,用于创建线程的等待时间并不会延迟任务的执行,因此提高响应性。通过适当地调整线程池的大小,你可以得到足够多的线程以保持处理器忙碌,同时可以还防止过多的线程互相竞争资源,导致应用程序耗尽内存或者失败。
每任务每线程例子如下:
public class ThreadPool { public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(80);
while (true){
final Socket socket = serverSocket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(socket);
}
};
new Thread(task).start();
}
} private static void handleRequest(Socket socket) {
} }
可以看到这个例子是一个粗制滥造的并发服务端,来一个用户就创建一个线程,你根本就不知道有多少用户来,要创建多少个线程。这样频繁创建线程就会导致大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,
过渡滥创建线程而耗尽有限资源。
由于这原因,java中给我们提供Executor框架。通过Executors中的某个静态工厂方法来创建一个线程池:
1.newFixedThreadPool 创建一个定长的线程池,每当提交一个任务就创建一个线程,知道达到池的最大长度,这时线程池会保持长度不再变化(如果一个线程由于非预期的Exception而结束,线程池会补充一个新的线程)。
下面用newFixedThreadPool 创建一个定长的线程池来改造上面的例子,如下:
public class ThreadPool { public static void main(String[] args) throws IOException {
//newFixedThreadPool参数为线程池的大小
Executor executor = Executors.newFixedThreadPool(100); ServerSocket serverSocket = new ServerSocket(80);
while (true){
final Socket socket = serverSocket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(socket);
}
};
//直接将任务丢进线程池来执行任务
executor.execute(task);
}
} private static void handleRequest(Socket socket) {
} }
这样就不会发生过渡滥创建线程而耗尽有限资源。
2.newSingleThreadExecutor创建一个单线程化的executor,他只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有另一个取代它。executor会保证任务依照任务队列所规定的顺序(FIFO,LIFO,优先级)执行。
3.newCachedThreadPool创建一个可缓存的线程池,如果当前线程的长度超过了处理的需要,它可以灵活的回收空闲的线程,当需求增加时,它可以灵活的增加新的线程,并不会对池的长度做任何限制。但是认为改线程池的长度没有任何限制,有可能会把资源耗尽,
这需要自己很好的把控了。
4.newScheduledThreadPool创建一个定长的线程池,而且支持定时的以及周期性的任务执行,类似于Timer。
Executor的生命周期:
Executor实现通常知识为执行任务而创建线程。但是JVM会在所有(非后台的,nondaemon)线程全部终止后才退出。因此,如果无法正确关闭Executor,将会阻止JVM的结束。
因为Executor是异步地执行任务,所以在任何时间里,所有之前提交的任务状态都不能立即可见。这些任务中,有些可能已经完成,有些可能正在运行,其他的还可能在队列中等待执行。关闭应用程序时,程序会出现很多中情况:从平缓关闭
到最突然的关闭,以及介于这两种阶段情况之间的各种可能。Executor是为应用服务提供服务的,他们理应可以关闭,无论是平缓还是突然。
注意:关闭操作还会影响到记录应用程序任务状态的反馈信息。
Executor就是一个接口,源码如下图:
我们可以进入Executors这个类的源码,如下:
可以看到newFixedThreadPool 创建一个定长的线程池,返回的是一个ExecutorService,但是我们上面例子接收的是Executor,为什么Executor也可以接收呢?我们继续进入ExecutorService源码如下:
可以看到原来ExecutorService继承了Executor。ExecutorService扩展了Executor,并且添加了一些用于声明周期管理的方法。
源码如下:
ExecutorService暗示了生命周期有3种状态:运行、关闭、终止。ExecutorService最初创建后的初始状态是运行状态。
shutdown方法会启动一个平缓的关闭过程:停止接受新的任务,同时等待已经提交的任务完成,包括尚未开始执行的任务。
shutdownNow方法会启动要给强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始执行的任务is。
isShutdown方法:判断线程池(即ExecutorService)是否关闭。
isTerminated方法:是线程池(即ExecutorService)是否进入终止状态。
在关闭后提交到ExecutorService中的任务,会被拒绝执行处理器(rejected execution handler)处理。拒绝执行处理器是ExecutorService的一种实现,ThreadPoolExecutor提供的,ExecutorService接口中的方法并不提供拒绝执行处理器。拒绝执行处理器可能只是
简单的放弃任务,也可能会引起execute抛出一个未检查的RejectedExecutionException。一旦所有任务全部完成后,ExecutorService会转入终止状态。
awaitTermination方法:等待ExecutorService到达终止状态。
通常shutdown会紧随awaitTermination之后,这样可以产生同步地关闭ExecutorService的效果。
上面的Executor的例子的程序是没办法关闭线程池,会一直跑下去,那么我们如何写一个支持关闭的webserver呢?
明显我们现在要用ExecutorService来改造上面的Executor的例子。伪代码如下:
public class ThreadPool {
private static ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) throws IOException { //newFixedThreadPool参数为线程池的大小 ServerSocket serverSocket = new ServerSocket(80);
//这里就不再像上面的例子一样无限的接受任务了,要根据我的线程池是否处于关闭状态来决定
while (!executorService.isShutdown()){
final Socket socket = serverSocket.accept();
try{
executorService.execute(new Runnable() {
public void run() {
handleRequest(socket);
}
});
}catch (RejectedExecutionException e){
//如果拒绝服务不是因为我线程池关闭导致的,我们要在这里打印一下日志
if (!executorService.isShutdown()){
System.out.println("接受任务被拒绝");
throw e;
}
}
}
} //用一个公共的方法去关闭线程池
public void stop(){
executorService.shutdown();
} private static void handleRequest(Socket socket) {
//获取请求
Request req = readRequest(socket);
//如果请求已经关闭
if (isShutdownRequest(req)){
//关闭线程池
stop();
}else {
//请求转发
dispatchRequest(req);
}
} }
经过改造,这服务端变的优雅多了。
延时的,并具有周期性的任务
在newScheduledThreadPool出来之前我们一般会用Timer和TimerTask来做。Timer在JDK里面,是很早的一个API了。
但是Timer存在一些缺陷,Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。例如一个TimerTask每10秒执行一次,
而另外一个TimerTask每40ms执行一次,重复出现的任务会在后市的任务完成后快速连续的被调用4次,要么完全“丢失”4次调用。
Timer的另外一个问题在于,如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时
已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。
现在我们看一下Timer的例子,如下:
public class Shedule {
private static long start; public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {
System.out.println(System.currentTimeMillis()-start);
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}; TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
}; Timer timer = new Timer();
start = System.currentTimeMillis();
//启动一个调度任务,1S钟后执行
timer.schedule(task,1000);
//启动一个调度任务,3S钟后执行
timer.schedule(task1,3000); } }
上面程序我们预想是第一个任务执行后,第二个任务3S后执行的,即输出一个1000,一个3000.
实际运行结果如下:
实际运行结果并不如我们所愿。世界结果,是过了4S后才输出第二个任务,即4001约等于4秒。那部分时间时间到哪里去了呢?那个时间是被我们第一个任务的sleep所占用了。
现在我们在第一个任务中去掉Thread.sleep();这一行代码,运行是否正确了呢?如下:
public class Shedule {
private static long start; public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {
System.out.println(System.currentTimeMillis()-start); }
}; TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
}; Timer timer = new Timer();
start = System.currentTimeMillis();
//启动一个调度任务,1S钟后执行
timer.schedule(task,1000);
//启动一个调度任务,3S钟后执行
timer.schedule(task1,3000); } }
运行结果如下:
可以看到确实是第一个任务过了1S后执行,第二个任务在第一个任务执行完后过3S执行了。
这就说明了Timer只创建唯一的线程来执行所有Timer任务。如果一个timer任务的执行很耗时,会导致其他TimerTask的时效准确性出问题。
Timer存在一些缺陷,因此你应该考虑使用ScheduledThreadPoolExecutor作为替代品。你可以通过构造函数或者通过newScheduledThreadPool工厂方法创建一个ScheduledThreadPoolExecutor。
如下:
public class Shedule {
private static long start; private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {
System.out.println(System.currentTimeMillis()-start);
try{
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}; TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
}; Timer timer = new Timer();
start = System.currentTimeMillis(); //TimeUnit.MILLISECONDS指定毫秒为单位
executorService.schedule(task,1000, TimeUnit.MILLISECONDS);
executorService.schedule(task1,3000, TimeUnit.MILLISECONDS); } }
运行结果如下:
可以看到运行结果符合预期。
可以看到如果一个timer任务的执行很耗时(例如Thread.sleep),ScheduledThreadPoolExecutor并不会导致其他TimerTask的时效准确性出问题。
还可以看到,这两个TimerTask互不干扰。
互相干扰还有一个反面:
如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时
已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。
例子如下:
public class Shedule {
private static long start; private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {
System.out.println(System.currentTimeMillis()-start);
throw new RuntimeException();
}
}; TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
}; Timer timer = new Timer();
start = System.currentTimeMillis(); timer.schedule(task,1000);
timer.schedule(task1,3000); } }
如果第一TimerTask出现未知异常,第二个TimerTask还能运行起来吗?
结果如下:
明显第一TimerTask出现未知异常,第二个TimerTask不能运行起来了。这就说明
如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时
已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了。
ScheduledThreadPoolExecutor可以解决此问题,例子如下:
public class Shedule {
private static long start; private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2); public static void main(String[] args) {
TimerTask task = new TimerTask() {
public void run() {throw new RuntimeException();
}
}; TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()-start);
}
}; Timer timer = new Timer();
start = System.currentTimeMillis(); //TimeUnit.MILLISECONDS指定毫秒为单位
executorService.schedule(task,1000, TimeUnit.MILLISECONDS);
executorService.schedule(task1,3000, TimeUnit.MILLISECONDS); } }
运行结果如下:
可以看到第一个线程挂了,第二个线程并没有受到影响。这就说明了ScheduledThreadPoolExecutor可以解决了
如果TimerTask抛出未检查的异常会终止timer线程。这种情况下,Timer也不会重新回复线程的执行了;它错误的认为整个Timer都被取消了。此时
已经被安排但尚未执行的TimerTask永远不会再执行了,新的任务也不能被调度了的问题
还要注意一点,Timer是和系统时间挂钩的,如果当前服务器的时间一改,Timer就不那么靠谱了。
还要注意的是ThreadPoolExecutor。
源码如下图:
可以看到new一个FixedThreadPool/newSingleThreadExecutor/newCachedThreadPool/newScheduledThreadPool实际上返回的都是是new ThreadPoolExecutor()。
我们再看一下ThreadPoolExecutor源码如下:
可以看到ThreadPoolExecutor配置的非常灵活,如果我们用普通的一个FixedThreadPool/newSingleThreadExecutor/newCachedThreadPool/newScheduledThreadPool没办法满足你的需求了,你可以用
ThreadPoolExecutor灵活的指定参数来完成你的需求。这适合精确的任务执行。还不如说我们的任务被拒绝(RejecedExecutionHandler)后,我们可以用ThreadPoolExecutor灵活处理
Java并发编程笔记4-线程池的更多相关文章
- 【Java并发编程六】线程池
一.概述 在执行并发任务时,我们可以把任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程,只要池里有空闲的线程,任务就会分配一个线程执行.在线程池的内部,任务被插入一个阻塞队列(Blo ...
- Java并发编程 (九) 线程调度-线程池
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 声明:实际上,在开发中并不会普遍的使用Thread,因为它具有一些弊端,对并发性能的影响比较大,如下: ...
- java并发编程笔记(七)——线程池
java并发编程笔记(七)--线程池 new Thread弊端 每次new Thread新建对象,性能差 线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或者OOM 缺 ...
- java并发编程笔记(五)——线程安全策略
java并发编程笔记(五)--线程安全策略 不可变得对象 不可变对象需要满足的条件 对象创建以后其状态就不能修改 对象所有的域都是final类型 对象是正确创建的(在对象创建期间,this引用没有逸出 ...
- java并发编程笔记(三)——线程安全性
java并发编程笔记(三)--线程安全性 线程安全性: 当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现 ...
- java并发编程笔记(九)——多线程并发最佳实践
java并发编程笔记(九)--多线程并发最佳实践 使用本地变量 使用不可变类 最小化锁的作用域范围 使用线程池Executor,而不是直接new Thread执行 宁可使用同步也不要使用线程的wait ...
- Java并发编程系列-(2) 线程的并发工具类
2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...
- java并发编程笔记(十)——HashMap与ConcurrentHashMap
java并发编程笔记(十)--HashMap与ConcurrentHashMap HashMap参数 有两个参数影响他的性能 初始容量(默认为16) 加载因子(默认是0.75) HashMap寻址方式 ...
- java并发编程笔记(八)——死锁
java并发编程笔记(八)--死锁 死锁发生的必要条件 互斥条件 进程对分配到的资源进行排他性的使用,即在一段时间内只能由一个进程使用,如果有其他进程在请求,只能等待. 请求和保持条件 进程已经保持了 ...
随机推荐
- BZOJ 1485: [HNOI2009]有趣的数列 [Catalan数 质因子分解]
1485: [HNOI2009]有趣的数列 Description 我们称一个长度为2n的数列是有趣的,当且仅当该数列满足以下三个条件: (1)它是从1到2n共2n个整数的一个排列{ai}: (2)所 ...
- Does Java pass by reference or pass by value?(Java是值传递还是引用传递) - 总结
这个话题一直是Java程序员的一个热议话题,争论不断,但是不论是你百度搜也好还是去看官方的文档中所标明的也好,得到的都只有一个结论:Java只有值传递. 在这里就不贴代码细致解释了,让我们来看看一些论 ...
- XML实体解析器的作用
XML实体解析器的作用 什么是实体解析器 如果一个sax解析器需要实现对外部实体的自定义处理,那么必须实现一个EntityResolver接口并且注册到SAX驱动上. 从这段文字可以看出来,实体解析器 ...
- VUE2.0 elemenui-ui 2.0.X 封装 省市区三级
1. 效果图 2. 版本依赖 vue 2.X , elementui 2.0.11 使用element ui <el-form>标签 3. 源码 components/CityL ...
- Trie树/字典树题目(2017今日头条笔试题:异或)
/* 本程序说明: [编程题] 异或 时间限制:1秒 空间限制:32768K 给定整数m以及n个数字A1,A2,..An,将数列A中所有元素两两异或,共能得到n(n-1)/2个结果,请求出这些结果中大 ...
- eclipse CDT unresolved inclusion
原因:c\c++库未设置所导致的 解决办法:1.先配置环境变量:打开window->preference->c\c++bulid->environment->add-> ...
- vue学习问题总结(一)
使用comopontent组件报错错误信息:vue.js:491 [Vue warn]: Unknown custom element: <todo-item> - did you reg ...
- Mybatis学习之道(一)
本例子为采用的mysql+maven+mybatis构建. 初步学习mybatis: mybatis为一个半自动框架,相对于hibernate来说他更加轻巧,学习成本更低. 1.新建一个maven工程 ...
- Elasticsearch-深入理解索引原理
最近开始大面积使用ES,很多地方都是知其然不知其所以然,特地翻看了很多资料和大牛的文档,简单汇总一篇.内容多为摘抄,说是深入其实也是一点浅尝辄止的理解.希望大家领会精神. 首先学习要从官方开始地址如下 ...
- java基础--面对对象
面对对象--概述 什么是对象? +---->对象可以泛指一切现实中存着的事物 +---->类是对象的抽象集合 什么是面对对象? +--->万物皆对象,面对对象实际就是人与万物接触== ...