`

Java多线程(十一)之线程池深入分析(上)

 
阅读更多

线程池是并发包里面很重要的一部分,在实际情况中也是使用很多的一个重要组件。

下图描述的是线程池API的一部分。广义上的完整线程池可能还包括Thread/Runnable、Timer/TimerTask等部分。这里只介绍主要的和高级的API以及架构和原理。

大多数并发应用程序是围绕执行任务(Task)进行管理的。所谓任务就是抽象、离散的工作单元(unit of work)。把一个应用程序的工作(work)分离到任务中,可以简化程序的管理;这种分离还在不同事物间划分了自然的分界线,可以方便程序在出现错误时进行恢复;同时这种分离还可以为并行工作提供一个自然的结构,有利于提高程序的并发性。下面通过任务的执行策略来引入Executor相关的介绍。

 

一、任务的执行策略

 

任务的执行策略包括4W3H部分:

  • 任务在什么(What)线程中执行
  • 任务以什么(What)顺序执行(FIFO/LIFO/优先级等)
  • 同时有多少个(How Many)任务并发执行
  • 允许有多少个(How Many)个任务进入执行队列
  • 系统过载时选择放弃哪一个(Which)任务,如何(How)通知应用程序这个动作
  • 任务执行的开始、结束应该做什么(What)处理

在后面的章节中会详细分写这些策略是如何实现的。我们先来简单回答些如何满足上面的条件。

  1. 首先明确一定是在Java里面可以供使用者调用的启动线程类是Thread。因此Runnable或者Timer/TimerTask等都是要依赖Thread来启动的,因此在ThreadPool里面同样也是靠Thread来启动多线程的。
  2. 默认情况下Runnable接口执行完毕后是不能拿到执行结果的,因此在ThreadPool里就定义了一个Callable接口来处理执行结果。
  3. 为了异步阻塞的获取结果,Future可以帮助调用线程获取执行结果。
  4. Executor解决了向线程池提交任务的入口问题,同时ScheduledExecutorService解决了如何进行重复调用任务的问题。
  5. CompletionService解决了如何按照执行完毕的顺序获取结果的问题,这在某些情况下可以提高任务执行的并发,调用线程不必在长时间任务上等待过多时间。
  6. 显然线程的数量是有限的,而且也不宜过多,因此合适的任务队列是必不可少的,BlockingQueue的容量正好可以解决此问题。
  7. 固定任务容量就意味着在容量满了以后需要一定的策略来处理过多的任务(新任务),RejectedExecutionHandler正好解决此问题。
  8. 一定时间内阻塞就意味着有超时,因此TimeoutException就是为了描述这种现象。TimeUnit是为了描述超时时间方便的一个时间单元枚举类。
  9. 有上述问题就意味了配置一个合适的线程池是很复杂的,因此Executors默认的一些线程池配置可以减少这个操作。


二、线程池Executor的类体系结构与常用线程池

 

 

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService

下面这张图完整描述了线程池的类体系结构。

 

首先Executor的execute方法只是执行一个Runnable的任务,当然了从某种角度上将最后的实现类也是在线程中启动此任务的。根据线程池的执行策略最后这个任务可能在新的线程中执行,或者线程池中的某个线程,甚至是调用者线程中执行(相当于直接运行Runnable的run方法)。这点在后面会详细说明。

ExecutorService在Executor的基础上增加了一些方法,其中有两个核心的方法:

  • Future<?> submit(Runnable task)
  • <T> Future<T> submit(Callable<T> task)

这两个方法都是向线程池中提交任务,它们的区别在于Runnable在执行完毕后没有结果,Callable执行完毕后有一个结果。这在多个线程中传递状态和结果是非常有用的。另外他们的相同点在于都返回一个Future对象。Future对象可以阻塞线程直到运行完毕(获取结果,如果有的话),也可以取消任务执行,当然也能够检测任务是否被取消或者是否执行完毕。

 

 

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在

Executors类里面提供了一些静态工厂,生成一些常用的线程池

  • newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
  • newSingleThreadScheduledExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

 

三、线程池Executor的数据结构

 

 

由于已经看到了ThreadPoolExecutor的源码,因此很容易就看到了ThreadPoolExecutor线程池的数据结构。下图3描述了这种数据结构。

图3 ThreadPoolExecutor 数据结构

其实,即使没有上述图形描述ThreadPoolExecutor的数据结构,我们根据线程池的要求也很能够猜测出其数据结构出来。

  • 线程池需要支持多个线程并发执行,因此有一个线程集合Collection<Thread>来执行线程任务;
  • 涉及任务的异步执行,因此需要有一个集合来缓存任务队列Collection<Runnable>;
  • 很显然在多个线程之间协调多个任务,那么就需要一个线程安全的任务集合,同时还需要支持阻塞、超时操作,那么BlockingQueue是必不可少的;
  • 既然是线程池,出发点就是提高系统性能同时降低资源消耗,那么线程池的大小就有限制,因此需要有一个核心线程池大小(线程个数)和一个最大线程池大小(线程个数),有一个计数用来描述当前线程池大小;
  • 如果是有限的线程池大小,那么长时间不使用的线程资源就应该销毁掉,这样就需要一个线程空闲时间的计数来描述线程何时被销毁;
  • 前面描述过线程池也是有生命周期的,因此需要有一个状态来描述线程池当前的运行状态;
  • 线程池的任务队列如果有边界,那么就需要有一个任务拒绝策略来处理过多的任务,同时在线程池的销毁阶段也需要有一个任务拒绝策略来处理新加入的任务;
  • 上面种的线程池大小、线程空闲实际那、线程池运行状态等等状态改变都不是线程安全的,因此需要有一个全局的锁(mainLock)来协调这些竞争资源;
  • 除了以上数据结构以外,ThreadPoolExecutor还有一些状态用来描述线程池的运行计数,例如线程池运行的任务数、曾经达到的最大线程数,主要用于调试和性能分析。

 

四、线程池Executor生命周期

 

 

线程池Executor是异步的执行任务,因此任何时刻不能够直接获取提交的任务的状态。这些任务有可能已经完成,也有可能正在执行或者还在排队等待执行。因此关闭线程池可能出现一下几种情况:

  • 平缓关闭:已经启动的任务全部执行完毕,同时不再接受新的任务
  • 立即关闭:取消所有正在执行和未执行的任务

另外关闭线程池后对于任务的状态应该有相应的反馈信息。

 

图4 描述了线程池的4种状态。

  • 线程池在构造前(new操作)是初始状态,一旦构造完成线程池就进入了执行状态RUNNING。严格意义上讲线程池构造完成后并没有线程被立即启动,只有进行“预启动”或者接收到任务的时候才会启动线程。这个会后面线程池的原理会详细分析。但是线程池是出于运行状态,随时准备接受任务来执行。
  • 线程池运行中可以通过shutdown()和shutdownNow()来改变运行状态。shutdown()是一个平缓的关闭过程,线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务,这时候线程池处于SHUTDOWN状态;shutdownNow()是一个立即关闭过程,线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务,这时候线程池处于STOP状态。
  • 一旦shutdown()或者shutdownNow()执行完毕,线程池就进入TERMINATED状态,此时线程池就结束了。
  • isTerminating()描述的是SHUTDOWN和STOP两种状态。
  • isShutdown()描述的是非RUNNING状态,也就是SHUTDOWN/STOP/TERMINATED三种状态。

 

图4

线程池的API如下:

图5

其中shutdownNow()会返回那些已经进入了队列但是还没有执行的任务列表。awaitTermination描述的是等待线程池关闭的时间,如果等待时间线程池还没有关闭将会抛出一个超时异常。

对于关闭线程池期间发生的任务提交情况就会触发一个拒绝执行的操作。这是java.util.concurrent.RejectedExecutionHandler描述的任务操作。下一个小结中将描述这些任务被拒绝后的操作。

 

总结下这个小节

  1. 线程池有运行、关闭、停止、结束四种状态,结束后就会释放所有资源
  2. 平缓关闭线程池使用shutdown()
  3. 立即关闭线程池使用shutdownNow(),同时得到未执行的任务列表
  4. 检测线程池是否正处于关闭中,使用isShutdown()
  5. 检测线程池是否已经关闭使用isTerminated()
  6. 定时或者永久等待线程池关闭结束使用awaitTermination()操作

 

五、线程池Executor任务拒绝策略

 

紧接上面,对于关闭线程池期间发生的任务提交情况就会触发一个拒绝执行的操作。这是java.util.concurrent.RejectedExecutionHandler描述的任务操作。

先来分析下为什么有任务拒绝的情况发生

这里先假设一个前提:线程池有一个任务队列,用于缓存所有待处理的任务,正在处理的任务将从任务队列中移除。因此在任务队列长度有限的情况下就会出现新任务的拒绝处理问题,需要有一种策略来处理应该加入任务队列却因为队列已满无法加入的情况。另外在线程池关闭的时候也需要对任务加入队列操作进行额外的协调处理。

 

RejectedExecutionHandler提供了四种方式来处理任务拒绝策略。

这四种策略是独立无关的,是对任务拒绝处理的四种表现形式。

最简单的方式就是直接丢弃任务。但是却有两种方式,到底是该丢弃哪一个任务,比如可以丢弃当前将要加入队列的任务本身(DiscardPolicy)或者丢弃任务队列中最旧任务(DiscardOldestPolicy)。丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些额外的处理。除了丢弃任务还可以直接抛出一个异常(RejectedExecutionException),这是比较简单的方式。抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用者的处理过程。除了抛出异常以外还可以不进入线程池执行,在这种方式(CallerRunsPolicy)中任务将有调用者线程去执行。

 

上面是一些理论知识,下面结合一些例子进行分析讨论。

 

[java] view plaincopy
 
  1. package xylz.study.concurrency;  
  2.   
  3. import java.lang.reflect.Field;  
  4. import java.util.concurrent.ArrayBlockingQueue;  
  5. import java.util.concurrent.ThreadPoolExecutor;  
  6. import java.util.concurrent.TimeUnit;  
  7. import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;  
  8. import java.util.concurrent.ThreadPoolExecutor.DiscardPolicy;  
  9.   
  10. public class ExecutorServiceDemo {  
  11.   
  12.     static void log(String msg) {  
  13.         System.out.println(System.currentTimeMillis() + " -> " + msg);  
  14.     }  
  15.   
  16.     static int getThreadPoolRunState(ThreadPoolExecutor pool) throws Exception {  
  17.         Field f = ThreadPoolExecutor.class.getDeclaredField("runState");  
  18.         f.setAccessible(true);  
  19.         int v = f.getInt(pool);  
  20.         return v;  
  21.     }  
  22.   
  23.     public static void main(String[] args) throws Exception {  
  24.   
  25.         ThreadPoolExecutor pool = new ThreadPoolExecutor(110, TimeUnit.SECONDS,  
  26.                 new ArrayBlockingQueue<Runnable>(1));  
  27.         pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());  
  28.         for (int i = 0; i < 10; i++) {  
  29.             final int index = i;  
  30.             pool.submit(new Runnable() {  
  31.   
  32.                 public void run() {  
  33.                     log("run task:" + index + " -> " + Thread.currentThread().getName());  
  34.                     try {  
  35.                         Thread.sleep(1000L);  
  36.                     } catch (Exception e) {  
  37.                         e.printStackTrace();  
  38.                     }  
  39.                     log("run over:" + index + " -> " + Thread.currentThread().getName());  
  40.                 }  
  41.             });  
  42.         }  
  43.         log("before sleep");  
  44.         Thread.sleep(4000L);  
  45.         log("before shutdown()");  
  46.         pool.shutdown();  
  47.         log("after shutdown(),pool.isTerminated=" + pool.isTerminated());  
  48.         pool.awaitTermination(1000L, TimeUnit.SECONDS);  
  49.         log("now,pool.isTerminated=" + pool.isTerminated() + ", state="  
  50.                 + getThreadPoolRunState(pool));  
  51.     }  
  52.   
  53. }  

 


第一种方式直接丢弃(DiscardPolicy)的输出结果是:

 

[java] view plaincopy
 
  1. 1294494050696 -> run task:0  
  2. 1294494050696 -> before sleep  
  3. 1294494051697 -> run over:0 -> pool-1-thread-1  
  4. 1294494051697 -> run task:1  
  5. 1294494052697 -> run over:1 -> pool-1-thread-1  
  6. 1294494054697 -> before shutdown()  
  7. 1294494054697 -> after shutdown(),pool.isTerminated=false  
  8. 1294494054698 -> now,pool.isTerminated=true, state=3  

 

 

对于上面的结果需要补充几点。

  1. 线程池设定线程大小为1,因此输出的线程就只有一个”pool-1-thread-1”,至于为什么是这个名称,以后会分析。
  2. 任务队列的大小为1,因此可以输出一个任务执行结果。但是由于线程本身可以带有一个任务,因此实际上一共执行了两个任务(task0和task1)。
  3. shutdown()一个线程并不能理解是线程运行状态位terminated,可能需要稍微等待一点时间。尽管这里等待时间参数是1000秒,但是实际上从输出时间来看仅仅等了约1ms。
  4. 直接丢弃任务是丢弃将要进入线程池本身的任务,所以当运行task0是,task1进入任务队列,task2~task9都被直接丢弃了,没有运行。

如果把策略换成丢弃最旧任务(DiscardOldestPolicy),结果会稍有不同。

 

[java] view plaincopy
 
  1. 1294494484622 -> run task:0  
  2. 1294494484622 -> before sleep  
  3. 1294494485622 -> run over:0 -> pool-1-thread-1  
  4. 1294494485622 -> run task:9  
  5. 1294494486622 -> run over:9 -> pool-1-thread-1  
  6. 1294494488622 -> before shutdown()  
  7. 1294494488622 -> after shutdown(),pool.isTerminated=false  
  8. 1294494488623 -> now,pool.isTerminated=true, state=3  

 

 

这里依然只是执行两个任务,但是换成了任务task0和task9。实际上task1~task8还是进入了任务队列,只不过被task9挤出去了。

对于异常策略(AbortPolicy)就比较简单,这回调用线程的任务执行。

对于调用线程执行方式(CallerRunsPolicy),输出的结果就有意思了。

 

[java] view plaincopy
 
  1. 1294496076266 -> run task:2 -> main  
  2. 1294496076266 -> run task:0 -> pool-1-thread-1  
  3. 1294496077266 -> run over:0 -> pool-1-thread-1  
  4. 1294496077266 -> run task:1 -> pool-1-thread-1  
  5. 1294496077266 -> run over:2 -> main  
  6. 1294496077266 -> run task:4 -> main  
  7. 1294496078267 -> run over:4 -> main  
  8. 1294496078267 -> run task:5 -> main  
  9. 1294496078267 -> run over:1 -> pool-1-thread-1  
  10. 1294496078267 -> run task:3 -> pool-1-thread-1  
  11. 1294496079267 -> run over:3 -> pool-1-thread-1  
  12. 1294496079267 -> run over:5 -> main  
  13. 1294496079267 -> run task:7 -> main  
  14. 1294496079267 -> run task:6 -> pool-1-thread-1  
  15. 1294496080267 -> run over:7 -> main  
  16. 1294496080267 -> run task:9 -> main  
  17. 1294496080267 -> run over:6 -> pool-1-thread-1  
  18. 1294496080267 -> run task:8 -> pool-1-thread-1  
  19. 1294496081268 -> run over:9 -> main  
  20. 1294496081268 -> before sleep  
  21. 1294496081268 -> run over:8 -> pool-1-thread-1  
  22. 1294496085268 -> before shutdown()  
  23. 1294496085268 -> after shutdown(),pool.isTerminated=false  
  24. 1294496085269 -> now,pool.isTerminated=true, state=3  
分享到:
评论

相关推荐

    java+socket 及多线程线程池应用(IBM教程)

    在这个“java+socket 及多线程线程池应用”的教程中,我们可以期待学习到以下核心知识点: 1. **Socket基础**:首先会讲解Socket的基本概念,包括服务器端Socket和客户端Socket的工作原理,以及TCP/IP协议在Socket...

    JAVAJAVA多线程教学演示系统论文

    《JAVA多线程教学演示系统》是一篇深入探讨JAVA多线程编程的论文,它针对教育领域中的教学需求,提供了一种生动、直观的演示方式,帮助学生更好地理解和掌握多线程技术。这篇论文的核心内容可能包括以下几个方面: ...

    java 多线程编程实战指南(核心 + 设计模式 完整版)

    《Java多线程编程实战指南》这本书深入浅出地讲解了Java多线程的核心概念和实战技巧,分为核心篇和设计模式篇,旨在帮助开发者掌握并应用多线程技术。 1. **线程基础** - **线程的创建**:Java提供了两种创建线程...

    Java、Android多线程、线程池Demo

    通过分析和理解这个示例,开发者可以更好地掌握Java和Android中多线程和线程池的使用,提升应用程序的性能和响应速度。在实际项目中,合理地配置和使用线程池能够有效优化程序的资源消耗,保证服务的稳定性和可扩展...

    Java多线程编程实战指南-核心篇

    《Java多线程编程实战指南-核心篇》是一本深入探讨Java并发编程的书籍,旨在帮助读者掌握在Java环境中创建、管理和同步线程的核心技术。Java的多线程能力是其强大之处,使得开发者能够在同一时间执行多个任务,提高...

    Java线程池及观察者模式解决多线程意外死亡重启问题

    Java线程池是Java并发编程中的重要组成部分,它...总之,通过Java线程池和观察者模式的结合,我们可以构建一个健壮的多线程系统,即使在部分线程意外终止的情况下,也能及时发现并采取措施恢复,确保系统的稳定运行。

    java多线程进阶

    Java多线程是Java编程中的核心概念,尤其对于高级开发者来说,掌握多线程的深入理解和应用至关重要。这本书“java多线程进阶”显然旨在帮助读者深化这方面的理解,打通编程中的“任督二脉”,使开发者能够更加熟练地...

    java多线程并发实战和源码

    Java多线程并发实战与源码分析是Java开发中至关重要的一部分,它涉及到程序性能优化、系统资源高效利用以及复杂逻辑的正确同步。本书主要聚焦于Java多线程的基础理论和实际应用,虽然书中实例和源码相对较少,但仍然...

    java多线程源码,仅供参考

    这个名为"java多线程源码,仅供参考"的压缩包文件,显然是为了帮助学习者深入理解Java多线程的运行机制。其中的示例程序"BounceThread"可能是一个演示线程交互和同步的经典案例,常用于教授线程的创建、运行以及线程...

    JAVA多线程端口扫描器

    **JAVA多线程端口扫描器** 在计算机网络中,端口扫描是一种常见的技术,用于检测目标主机上开放的服务和应用程序。此项目是基于Java语言实现的多线程端口扫描器,它允许用户对本地系统或指定的远程IP地址进行快速...

    多线程(线程池)的相关研究资料

    在计算机科学领域,多线程和线程池是并发编程中的关键概念,它们极大地提高了程序的执行效率和系统资源的利用率。线程是操作系统分配CPU时间的基本单位,而线程池则是管理和调度线程的一种机制。 多线程是指在一个...

    Java实现的线程池、消息队列功能

    线程池是一种多线程处理形式,预先创建一定数量的线程,然后将任务分配给这些线程执行。这样可以避免频繁创建和销毁线程带来的开销,提高系统效率。在Java中,`java.util.concurrent`包下的`ExecutorService`、`...

    CVI学习文件-多线程 线程池(修改增加学习版)

    5. **性能优化**:分析多线程和线程池对程序性能的影响,包括CPU利用率、内存消耗、响应时间等,根据实际需求调整线程池参数。 通过这个学习文件,你将能掌握如何在CVI项目中合理使用多线程和线程池,提升系统的...

    Java多线程聊天

    Java多线程聊天程序是一种利用Java编程语言实现的并发通信应用,它允许多个用户在同一时间进行交互式的对话。在这个程序中,多线程技术被用来处理并发用户输入和消息传递,确保系统的高效运行和响应性。下面将详细...

    java多线程作业.docx

    ### Java多线程知识点解析 #### 一、Java多线程概述 Java作为一种现代编程语言,内置了对多线程的支持。多线程允许应用程序同时处理多个任务,从而提高程序的响应性和整体性能。在多线程环境中,一个程序可以包含...

    Java多线程设计模式(带源码)

    本资源提供了详细的Java多线程设计模式的解析,包括源码分析,帮助开发者深入理解并熟练应用这些模式。 在多线程环境中,设计模式是解决常见问题的最佳实践,它们可以帮助开发者创建高效、可维护的并发代码。以下是...

    JAVA多线程教材

    10. **实战案例分析**:通过分析和解决实际问题,如Web服务器的并发处理、数据库连接池管理等,可以更好地理解和应用Java多线程技术。 以上只是《JAVA多线程教材》可能涵盖的部分内容,实际教材可能会更深入地讨论...

    java实现多线程文件传输

    在Java编程语言中,实现多线程文件传输是一种优化程序性能、提高系统资源...在提供的`java多线程文件传输`压缩包中,可能包含了实现这些概念的示例代码,通过分析和学习,可以更好地理解多线程文件传输的原理和实践。

    java多线程实例 代码可执行 绝对开源

    Java多线程是Java编程中的核心概念,尤其在开发高性能、高并发的应用...对于想要深入理解Java多线程和网络编程的开发者来说,这是一个很好的实践案例。通过分析和运行提供的源代码,你可以更好地理解和掌握这些技术。

    java线程池的源码分析.zip

    Java线程池是Java并发编程中的重要组成部分,它在多线程和高并发场景下扮演着关键角色。本文将深入探讨Java线程池的源码分析,并对比不同类型的线程池,以帮助开发者更好地理解和利用这一强大的工具。 首先,我们要...

Global site tag (gtag.js) - Google Analytics