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,则仅当队列满时才创建新线程,如果没满的话,那么就把Runnable放入队
列,等待执行; 如果设置的corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。
ThreadPoolExecutor是Executors类的实现,Executors类里面提供了一些静态工厂,生成一些常用的线程池,主
要有以下几个:
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行
所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它(意思是不同的线程id)。此线程池保证所有任务的执行顺序按照任
务的提交顺序执行。
newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线
程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,(意思是任务线程比线线程池大小小的
话)那么就会回收部分空闲(60秒不执行任务)的线程(所以只有newCachedThreadPool才设置了KeepALiveTime),当任务数增加时,此线程池
又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说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无界队列使用
public class ThreadPool { private final static String poolName = "mypool"; static private ThreadPool threadFixedPool = new ThreadPool(2); private ExecutorService executor; static public ThreadPool getFixedInstance() { return threadFixedPool; } private ThreadPool(int num) { executor = Executors.newFixedThreadPool(num, new DaemonThreadFactory(poolName)); } public void execute(Runnable r) { executor.execute(r); } public static void main(String[] params) { class MyRunnable implements Runnable { public void run() { System.out.println("OK!"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } for (int i = 0; i < 10; i++) { ThreadPool.getFixedInstance().execute(new MyRunnable()); } try { Thread.sleep(2000); System.out.println("Process end."); } catch (InterruptedException e) { e.printStackTrace(); } } }
在这段代码中,我们发现我们用到了Executors.newFixedThreadPool()函数,这个函数的实现是这样子的:
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
它实际上是创建了一个无界队列的固定大小的线程池。执行这段代码,我们发现所有的任务都正常处理了。但是在真实的线上环
境中会存在这样的一个问题,前端的用户请求源源不断的过来,后端的处理线程如果处理时间变长,无法快速的将用户请求处理
完返回结果给前端,那么任务队列中将堵塞大量的请求。这些请求在前端都是有超时时间设置的,假设请求是通过套接字过来,
当我们的后端处理进程处理完一个请求后,从队列中拿下一个任务,发现这个任务的套接字已经无效了,这是因为在用户端已经
超时,将套接字建立的连接关闭了。这样一来我们这边的处理程序再去读取套接字时,就会发生I/0 Exception. 恶性循环,导致我
们所有的处理服务线程读的都是超时的套接字,所有的请求过来都抛I/O异常,这样等于我们整个系统都挂掉了,已经无法对外提供
正常的服务了。
对于海量数据的处理,现在业界都是采用集群系统来进行处理,当请求的数量不断加大的时候,我们可以通过增加处理节点,反正现
在硬件设备相对便宜。但是要保证系统的可靠性和稳定性,在程序方面我们还是可以进一步的优化的,我们下一节要讲述的就是针对
线上出现的这类问题的一种处理策略。
上面newFixedThreadPool使用的是无界队列LinkedBlockingQueue,下面newCachedThreadPool使用的也是无界队列确实SynchronousQueue,最大线程池有几个就能运行几个,如果创建的线程数多于4040左右吧(我的pc是这样的)就内存溢出了
。
public class ThreadPoolTest {
private static ExecutorService mExcutorService;
private static final int SLEEP_TIME = 5000;
private static final int THREAD_COUNT = 4040;
public static void generateExecutor(){
mExcutorService = Executors.newCachedThreadPool();
}
public static void main(String[] args) {
ThreadPoolTest test = new ThreadPoolTest();
generateExecutor();
try {
for(int i=0;i<THREAD_COUNT;i++){
mExcutorService.execute(test.new WorkTask());
}
} catch (Exception e) {
e.printStackTrace();
}
}
class WorkTask implements Runnable{
public void run() {
try {
System.out.println(""+Thread.currentThread().getName()+"woring");
Thread.sleep(SLEEP_TIME)
);
System.out.println(""+Thread.currentThread().getName()+"stop woring");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4、ThreadPool用固定队列 ArrayBlockingQueue
public class ThreadPoolTest { private static ExecutorService mExcutorService; private static final int SLEEP_TIME = 5000; private static final int THREAD_COUNT = 13; public static void generateExecutor(){ ArrayBlockingQueue<Runnable> mArrayBlockingQueue = new ArrayBlockingQueue<Runnable>(8); mExcutorService = new ThreadPoolExecutor(2, 4, 60L, TimeUnit.SECONDS, mArrayBlockingQueue); } public static void main(String[] args) { ThreadPoolTest test = new ThreadPoolTest(); generateExecutor(); try { for(int i=0;i<THREAD_COUNT;i++){ mExcutorService.execute(test.new WorkTask()); } } catch (Exception e) { e.printStackTrace(); } } class WorkTask implements Runnable{ public void run() { try { System.out.println(""+Thread.currentThread().getName()+"woring"); Thread.sleep(10); System.out.println(""+Thread.currentThread().getName()+"stop woring"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
方法 execute(Runnable) 中提交时, 如果运行的线程少于 corePoolSize,则创建新线程来处理请求。 如果运行的线程多于
corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程,如果此时线程数量达到maximumPoolSize,并且队
列已经满,就会拒绝继续进来的请求。
现在是13个已经超出最大线程数+队列的存储数,那么第13个线程就会被拒绝而发生异常,如果线程数调整为11个那么就有固定的3个线程执行(创建新线程来处理请求, 如果运行的线程多于corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程).