- 浏览: 30456 次
文章分类
最新评论
前言:最近在做分布式海量数据处理项目,使用到了java的线程池,所以搜集了一些资料对它的使用做了一下总结和探究,
前面介绍的东西大多都是从网上搜集整理而来。文中最核心的东西在于后面两节无界队列线程池和有界队列线程池的实例
使用以及线上问题处理方案。
1. 为什么要用线程池?
在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器
在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在实际处理实际的用户请求的时间和资源要多的多。除
了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM中创建太多的线程,可能会导致系统由于
过度消耗内存或者“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻
处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象
来进行服务,这就是“池化资源”技术产生的原因。
线程池主要用来解决线程生命周期开销问题和资源不足问题,通过对多个任务重用线程,线程创建的开销被分摊到多个任
务上了,而且由于在请求到达时线程已经存在,所以消除了创建所带来的延迟。这样,就可以立即请求服务,使应用程序响
应更快。另外,通过适当的调整线程池中的线程数据可以防止出现资源不足的情况。
网上找来的这段话,清晰的描述了为什么要使用线程池,使用线程池有哪些好处。工程项目中使用线程池的场景比比皆是。
本文关注的重点是如何在实战中来使用好线程池这一技术,来满足海量数据大并发用户请求的场景。
2. ThreadPoolExecutor类
Java中的线程池技术主要用的是ThreadPoolExecutor 这个类。先来看这个类的构造函数,
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize 线程池维护线程的最少数量
maximumPoolSize 线程池维护线程的最大数量
keepAliveTime 线程池维护线程所允许的空闲时间
workQueue 任务队列,用来存放我们所定义的任务处理线程
threadFactory 线程创建工厂
handler 线程池对拒绝任务的处理策略
ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize 设置的边界自动调整池大小。当新任务在方法
execute(Runnable) 中提交时, 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是
空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。 如果设置的
corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。
ThreadPoolExecutor是Executors类的实现,Executors类里面提供了一些静态工厂,生成一些常用的线程池,主
要有以下几个:
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行
所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任
务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线
程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分
空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池
大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
在实际的项目中,我们会使用得到比较多的是newFixedThreadPool,创建固定大小的线程池,但是这个方法在真实的线上
环境中还是会有很多问题,这个将会在下面一节中详细讲到。
当任务源源不断的过来,而我们的系统又处理不过来的时候,我们要采取的策略是拒绝服务。RejectedExecutionHandler接
口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。
1)CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。
2)AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException();
}
这种策略直接抛出异常,丢弃任务。
3)DiscardPolicy:不能执行的任务将被删除
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}
这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
4)DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,
则重复此过程)
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略
需要适当小心。
3. ThreadPoolExecutor无界队列使用
01 public class ThreadPool {
02
03 private final static String poolName = "mypool";
04
05 static private ThreadPool threadFixedPool = new ThreadPool(2);
06
07 private ExecutorService executor;
08
09 static public ThreadPool getFixedInstance() {
10
11 return threadFixedPool;
12
13 }
14
15 private ThreadPool(int num) {
16
17 executor = Executors.newFixedThreadPool(num, new DaemonThreadFactory(poolName));
18
19 }
20
21 public void execute(Runnable r) {
22
23 executor.execute(r);
24
25 }
26
27 public static void main(String[] params) {
28
29 class MyRunnable implements Runnable {
30
31 public void run() {
32
33 System.out.println("OK!");
34
35 try {
36
37 Thread.sleep(10);
38
39 } catch (InterruptedException e) {
40
41 e.printStackTrace();
42
43 }
44
45 }
46
47 }
48
49 for (int i = 0; i < 10; i++) {
50
51 ThreadPool.getFixedInstance().execute(new MyRunnable());
52
53 }
54
55 try {
56
57 Thread.sleep(2000);
58
59 System.out.println("Process end.");
60
61 } catch (InterruptedException e) {
62
63 e.printStackTrace();
64
65 }
66
67 }
68
69 }
在这段代码中,我们发现我们用到了Executors.newFixedThreadPool()函数,这个函数的实现是这样子的:
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
它实际上是创建了一个无界队列的固定大小的线程池。执行这段代码,我们发现所有的任务都正常处理了。但是在真实的线上环
境中会存在这样的一个问题,前端的用户请求源源不断的过来,后端的处理线程如果处理时间变长,无法快速的将用户请求处理
完返回结果给前端,那么任务队列中将堵塞大量的请求。这些请求在前端都是有超时时间设置的,假设请求是通过套接字过来,
当我们的后端处理进程处理完一个请求后,从队列中拿下一个任务,发现这个任务的套接字已经无效了,这是因为在用户端已经
超时,将套接字建立的连接关闭了。这样一来我们这边的处理程序再去读取套接字时,就会发生I/0 Exception. 恶性循环,导致我
们所有的处理服务线程读的都是超时的套接字,所有的请求过来都抛I/O异常,这样等于我们整个系统都挂掉了,已经无法对外提供
正常的服务了。
对于海量数据的处理,现在业界都是采用集群系统来进行处理,当请求的数量不断加大的时候,我们可以通过增加处理节点,反正现
在硬件设备相对便宜。但是要保证系统的可靠性和稳定性,在程序方面我们还是可以进一步的优化的,我们下一节要讲述的就是针对
线上出现的这类问题的一种处理策略。
4. ThreadPoolExecutor有界队列使用
01 public class ThreadPool {
02
03 private final static String poolName = "mypool";
04
05 static private ThreadPool threadFixedPool = null;
06
07 public ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(2);
08
09 private ExecutorService executor;
10
11
12
13 static public ThreadPool getFixedInstance() {
14
15 return threadFixedPool;
16
17 }
18
19 private ThreadPool(int num) {
20
21 executor = new ThreadPoolExecutor(2, 4,60,TimeUnit.SECONDS, queue,new DaemonThreadFactory
22
23 (poolName), new ThreadPoolExecutor.AbortPolicy());
24
25 }
26
27 public void execute(Runnable r) {
28
29 executor.execute(r);
30
31 }
32
33
34
35 public static void main(String[] params) {
36
37 class MyRunnable implements Runnable {
38
39 public void run() {
40
41 System.out.println("OK!");
42
43 try {
44
45 Thread.sleep(10);
46
47 } catch (InterruptedException e) {
48
49 e.printStackTrace();
50
51 }
52
53 }
54
55 }
56
57 int count = 0;
58
59 for (int i = 0; i < 10; i++) {
60
61 try {
62
63 ThreadPool.getFixedInstance().execute(new MyRunnable());
64
65 } catch (RejectedExecutionException e) {
66
67 e.printStackTrace();
68
69 count++;
70
71 }
72
73 }
74
75 try {
76
77 log.info("queue size:" + ThreadPool.getFixedInstance().queue.size());
78
79 Thread.sleep(2000);
80
81 } catch (InterruptedException e) {
82
83 e.printStackTrace();
84
85 }
86
87 System.out.println("Reject task: " + count);
88
89 }
90
91 }
首先我们来看下这段代码几个重要的参数,corePoolSize 为2,maximumPoolSize为4,任务队列大小为2,每个任务平
均处理时间为10ms,一共有10个并发任务。
执行这段代码,我们会发现,有4个任务失败了。这里就验证了我们在上面提到有界队列时候线程池的执行顺序。当新任务在
方法 execute(Runnable) 中提交时, 如果运行的线程少于 corePoolSize,则创建新线程来处理请求。 如果运行的线程多于
corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程,如果此时线程数量达到maximumPoolSize,并且队
列已经满,就会拒绝继续进来的请求。
现在我们调整一下代码中的几个参数,将并发任务数改为200,执行结果Reject task: 182,说明有18个任务成功了,线程处理
完一个请求后会接着去处理下一个过来的请求。在真实的线上环境中,会源源不断的有新的请求过来,当前的被拒绝了,但只要线
程池线程把当下的任务处理完之后还是可以处理下一个发送过来的请求。
通过有界队列可以实现系统的过载保护,在高压的情况下,我们的系统处理能力不会变为0,还能正常对外进行服务,虽然有些服
务可能会被拒绝,至于如何减少被拒绝的数量以及对拒绝的请求采取何种处理策略我将会在下一篇文章《系统的过载保护》中继续
阐述。
参考文献:
ThreadPoolExecutor使用与思考(上)-线程池大小设置与BlockedQueue的三种实现区别 http://dongxuan.iteye.com/blog/901689
ThreadPoolExecutor使用与思考(中)-keepAliveTime及拒绝策略http://dongxuan.iteye.com/blog/902571
ThreadPoolExecutor源代码
Java线程池介绍以及简单实例 http://wenku.baidu.com/view/e4543a7a5acfa1c7aa00cc25.html
转自:http://www.cnblogs.com/cstar/archive/2012/06/14/2549494.html
相关推荐
java线程池使用后到底要关闭吗 java线程池是一种高效的并发编程技术,可以帮助开发者更好地管理线程资源,提高系统的性能和可靠性。然而,在使用java线程池时,一个常见的问题是:使用完线程池后到底要不要关闭?...
通过合理使用Java线程池,开发者可以更加高效地管理线程资源,提高应用程序的性能和稳定性。线程池的使用也应当注意避免资源竞争、线程死锁以及可能的内存泄漏等问题,确保线程安全和高效的并行处理能力。
"Java 线程池完整代码解析" Java 线程池是 Java 语言中的一个重要概念,它允许开发者创建和管理多个线程,以提高程序的并发性和性能。下面是对给定文件的解析,包括 title、description、标签和部分内容的解析。 ...
简单的线程池程序+中文文档 包结构: com.tangkai.threadpool --SimpleThread.java 工作线程 --TestThreadPool.java 程序入口 --ThreadPoolManager.java 线程池管理类
总之,Java线程池提供了一种强大的工具来管理和优化并发任务的执行,理解并熟练使用各种线程池实例能够显著提升程序的效率和可维护性。在设计系统时,应该充分考虑线程池的选择和配置,以适应不同类型的异步任务需求...
Java线程池是Java语言中一个非常重要的特性,特别是在多线程编程中,它通过管理和控制线程的创建和执行,有效地提高了程序的性能和资源利用率。线程池的引入始于JDK 1.5,它引入了`java.util.concurrent`包,提供了`...
Java线程池是一种高效管理线程的技术,它允许开发者预定义一组线程,根据任务的需要灵活调度,而不是每次需要执行任务时都创建新的线程。这种设计模式大大提高了系统的性能,减少了系统资源的消耗,特别是在高并发...
java线程池知识、
2.然后根据提示运行java命令执行示例程序,观看线程池的运行结果 目标:Java中多线程技术是一个难点,但是也是一个核心技术。因为Java本身就是一个多线程语言。本人目前在给46班讲授Swing的网络编程--使用Swing来...
要理解`java线程池threadpool简单使用源码`,你需要查看`src`目录下的Java文件,了解如何实例化`ThreadPoolExecutor`,设置相关参数,以及如何提交任务到线程池。同时,查看源码中对`ThreadGroup`的使用,理解它如何...
基于Java线程池技术实现Knock Knock游戏项目.zip 基于Java线程池技术实现Knock Knock游戏项目.zip 基于Java线程池技术实现Knock Knock游戏项目.zip 基于Java线程池技术实现Knock Knock游戏项目.zip 基于Java线程池...
讲述了java线程池的优点,参数,6种线程池的使用场景,线程池用到的handler,线程任务的提交方式等等。
总之,Java线程池是实现多线程并行处理的关键工具,理解和熟练使用它可以显著提高程序的并发性能,降低系统的资源消耗。通过深入学习和实践,我们可以更好地利用线程池来优化我们的Java应用程序。
本文将深入探讨如何在Java中使用线程池来查询大量数据,以及这样做的好处和实现方法。 首先,理解线程池的概念至关重要。线程池是一种多线程处理形式,预先创建了若干个线程,当有任务需要执行时,会从线程池中取出...
### 自定义实现Java线程池 #### 一、概述 在深入探讨自定义Java线程池之前,我们先简要回顾一下线程池的基本概念及其重要性。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动...
Java线程池是Java并发编程中的重要组成部分,它在多线程和高并发场景下扮演着关键角色。本文将深入探讨Java线程池的源码分析,并对比不同类型的线程池,以帮助开发者更好地理解和利用这一强大的工具。 首先,我们要...
java技术学习——基于Java线程池技术实现Knock Knock游戏项目(包含服务端、客户端两部分) java技术学习——基于Java线程池技术实现Knock Knock游戏项目(包含服务端、客户端两部分) java技术学习——基于Java...
Java线程池使用与原理详解 Java线程池是Java语言中的一种高级机制,用于管理和复用线程,以提高程序的执行效率和性能。本文将详细介绍Java线程池的使用和原理,包括线程池的概念、线程池的优点、线程池的创建、...
Java线程池是一种高效管理线程的机制,它允许开发者预先设定线程的数量,并通过池化的方式重用已创建的线程,以提高系统性能,减少线程的创建和销毁开销。线程池在Java中是通过`java.util.concurrent`包下的`...