声明:本文参考自:https://www.cnblogs.com/liconglong/p/13167227.html
Java中线程池的实现原理及流程:
如上图所示,当一个线程提交到线程池时(execute()或submit()),先判断核心线程数(corePoolSize)是否已满,如果未满,则直接创建线程执行任务;如果已满,则判断队列(BlockingQueue)是否已满,如果未满,则将线程添加到队列中;如果已满,则判断线程池(maximumPoolSize)是否已满,如果未满,则创建线程池执行任务;如果线程池已满,则交给饱和策略(RejectedExecutionHandler.rejectExcution())来处理。
可以看下线程池ThreadPoolExecutor的全参构造函数源码:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
对其入参释义如下:
参数 | 描述 | 作用 |
corelPoolSize | 线程核心线程数 | 当一个任务提交到线程池时,线程池会创建一个线程来执行任务,即使其他的核心线程足够执行新任务,也会创建线程,直到需要执行的任务数大于核心线程数后才不再创建;如果线程池先调用了preStartAllCoreThread()方法,则会先启动所有核心线程。 |
maximumPoolSize | 线程池最大线程数 | 如果队列满了,并且已创建的线程数小于该值,则会创建新的线程执行任务。这里需要说明一点,如果使用的队列是无界队列,那么该值无用。 |
keepAliveTime | 存活时间 | 当线程池中线程超过超时时间没有新的任务进入,则停止该线程;只会停止多于核心线程数的那几个线程。 |
unit | 线程存活的时间单位 | 可以有天、小时、分钟、秒、毫秒、微妙、纳秒 |
workQueue | 任务队列 |
用于保存等待执行任务的阻塞队列。可以选择如下几个队列:数组结构的有界队列ArrayBlockingQueue、链表结果的有界队列LinkedBlockingQueue、不存储元素的阻塞队列SynchronousQueue、一个具有优先级的无界阻塞队列PriortyBlockingQueue |
threadFactory | 创建线程的工厂 |
可以通过工厂给每个线程创建更有意义的名字。使用Guava提供的ThreadFactoryBuilder可以快速的给线程池里的线程创建有意义的名字,代码如下 new ThreadFactoryBuilder().setNameFormat("aaaaaaaa").build(); |
handler | 饱和策略 |
当队列和线程都满了,说明线程池处于饱和状态,那么必须采取一种策略来处理新提交的任务。 AbortPolicy(默认),表示无法处理新任务时抛出异常。 CallerRunsPolicy:只有调用者所在线程来运行 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务 DiscardPolicy:不处理,直接丢弃 |
上面说到,向线程池提交任务有两种方法,分别是execute()和submit(),两者的区别主要是execute()提交的是不需要有返回值的任务,而submit提交的是需要有返回值的任务,并且submit()会返回一个Furure对象,并且可以使用future.get()方法获取返回值,并且get方法会阻塞,直到有返回值。
线程池的关闭有shutdown()和shutdownNow两个方法,他们的原理是遍历线程池中的工作线程,然后逐个调用interrupt方法来中断线程,所以无法中断的线程可能永远无法终止;但是二者也有区别,shutdownNow是将线程池的状态设置为STOP,然后尝试停止所有正在执行或者暂停的线程,并返回等待执行任务列表;而shutdown只是将线程池的状态设置成SHUTDOWN,然后中断所有没有正在执行的任务。当调用这两个方法中的任何一个后,isShutdown方法就会返回true,当所有任务都已经关闭后,调用isTerminaed方法会返回true。
使用线程池时,需要从任务的性质(IO密集型还是CPU密集型或是混合型)、任务的优先级、任务的执行时常、任务的依赖性(是否依赖其他系统资源,如数据库连接等)来综合判断,比如说,CPU密集型,就可以就可以配置N+1个线程个数,其中N为CPU核数,如果是IO密集型,则可以配置2*N个线程数;如果是混合型的任务,可以将其拆分成IO密集型和CPU密集型,但是如果两个任务的执行时间相差较大,则没有必要进行拆分;优先级不同的任务可以使用优先级队列PriortyBlockingQueue来处理;依赖数据出等其它资源的线程池,比如说依赖数据库,那么就可以加大线程数量,因为在等待sql执行的时候,线程是处于空闲状态;另外,最好使用有界队列,因为有界队列可以增加系统的稳定性和预警能力。
对于线程的监控,还有以下几个方法可以使用:
方法 | 描述 |
taskCount() | 线程池需要执行的任务数量 |
completedTskCount | 线程池运行过程中已经执行完毕的任务数量 |
IarestPoolSize | 线程池中曾经创建过的最大线程数 |
getPoolSize | 线程池的线程数量 |
getActiveCount | 获取活动的线程数 |
Exector框架:
在java中,是用线程来异步执行任务,java线程的创建与销毁需要一定的开销。如果我们为每一个任务创建一个线程的话,这些线程就会消耗大量的计算资源,会使处于高负荷的应用崩溃。
在HotSpot虚拟机中,JAVA线程被一对一的映射为本地操作系统线程。JAVA线程启动时会创建一个本地操作系统线程,当该JAVA线程终止时,这个操作系统线程也会被收回,操作系统会调用多余线程并将他们分配给可用的CPU。
Executor框架的两级调度模型如上图所示,应用程序通过Executor控制上层的调度,而下层的调用由操作系统内核控制,将线程映射到硬件处理器上,下层的调用不受应用程序的控制。
关于Executor的组成部分如下所示:
元素 | 描述 |
任务 | 包括被执行任务需要实现的接口Runnable和Callable接口 |
任务的执行 | 包括任务执行机制的核心接口Executor,以及继承自Executor的ExecutorService接口。Executor接口有两个关键的实现类实现了ExecutorService接口:ThreadPoolExecutor和ScheduledThreadPoolExecutor |
异步计算的结果 | 包括接口Future和实现Future接口的FurureTask类 |
Executor框架使用示意图如下:
如上图所示,主线程首先创建实现Runnable或Callable接口的任务对象,然后把任务对象提交给ExecutorService执行,如果使用的是submit提交,执行完毕后将返回一个实现Future接口的对象,最后,主线程可以执行FutureTask.get()方法来获取返回值;主线程也可以调用FutureTask.cancel()方法来取消此任务的执行。
Executor框架的成员如下:
成员 | 描述 | 子类 | 描述 |
ThreadPoolExecutor |
通常使用工厂类Executors来创建,Executors可以创建三种类型的ThreadPoolExecutor |
固定线程数的FixedThreadPool |
适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的应用。 |
单一线程的SingleThreadPool | 适用于需要保证顺序的执行各个任务,并且在任意时间点都不会有多个线程活动的场景。 | ||
根据需要创建线程的CacheThreadPool | 这是一个无界的线程池,适用于执行很多短期异步任务的小程序,或者是负载比较轻的服务器。 | ||
ScheduledThreadPoolExecutor | 通常使用工厂类Executors创建,Executors可以创建两种类型的ScheduledThreadPoolExecutor | 包含若干线程的ScheduledThreadPoolExecutor | 适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程数量的应用场景。 |
只包含一个线程的SingleThreadScheduledExecutor | 适用于需要单个后台线程执行周期任务,同时需要保证顺序的执行各个任务的场景。 | ||
ForkJoinsPool |
newWorkStealingPool适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中 |
||
Future | Future接口和实现了该接口的FutureTask类来表示异步计算的结果 | ||
Runnable和Callable接口 |
Runnable和Callable接口的实现类,都可以被ThreadPoolExecutor、ScheduledThreadPool、ForkJoinThred执行;除了可以自己实现Callable接口外,我们还可以使用工厂类Executors来把一个Runnable包装成一个Callable |
ThreadPoolExecutor详解:
1、ThreadPoolExecutor
(1)FixedThreadPool
构造函数如下:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
构造函数中,核心线程数和最大线程数一致,keepAliveTime为0,队列使用的是无界阻塞队列LinkedBlockingQueue(最大值是Integer.MAX_VALUE);
核心线程数和最大线程数保持一致,表明:如果队列满了之后,不会再创建新的线程;
keepAliveTime为0,表明:如果运行线程数大于核心线程数时,如果线程执行完毕,空闲线程立刻被终止;
使用无界阻塞队列,表明:当运行线程到达核心线程数时,不会再创建线程,只会将任务加入阻塞队列;因此最大线程数参数无效;因此keepAliveTime参数无效;且不会拒绝任务(既不会执行包和策略)
(2)SingleThreadExecutor
构造函数如下:
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
构造函数中,核心线程数和最大线程数均为1,keepAliveTime为0,队列使用的是无界阻塞队列LinkedBlockingQueue(最大值是Integer.MAX_VALUE)
除了固定了核心线程数和最大线程数为1外,其余的参数均与FixedThreadPool一致,那么就是只有一个线程会反复循环从阻塞队列中获取任务执行
(3)CacheThreadPool
构造函数如下:
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
构造函数中,核心线程数为0,最大线程数为Integer.MAX_VALUE,意味着无界,keepAliveTime为60秒,阻塞队列使用没有存储空间的SynchronousQueue,核心线程数为0,最大线程数为无界,表明:只要队列满了,就会创建新的线程放入线程池,使用没有存储空间的SynchronousQueue表明:线程提交的速度高于线程被消费的速度,那么线程会被不断的创建,最终会因为线程创建过多而耗尽CPU和内存资源.
2、ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor的运行机制如下:
(1)当调用ScheduledTreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFuture接口的ScheduledFutureTask
(2)线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。
ScheduledFutureTask主要包含以下三个成员变量
成员变量 | 描述 |
long time | 表示这个任务要被执行的时间 |
long sequenceNumber | 表示该任务被添加到ScheduledThreadPoolExecutor中的序号 |
long period | 表示任务执行的间隔周期 |
DelayQueue封装了一个PriorityQueue,当添加任务时,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序,time最小的在最前面(最先被执行),如果time一致,就比较sequenceNumber,sequenceNumber小的排在前面。
当线程执行任务时,先从DelayQueue队列中获取已经到期的任务(time大于当前时间),然后执行该任务,执行完毕后,根据任务的执行周期,修改任务下次的执行时间time,并重新将任务添加到DelayQueue
FutureTask详解:
Future接口和实现该接口的FutureTask类,代表异步计算的结果。
FutureTask的使用方法是将其交给Executor执行,也可以通过ExecutorService.submit()方法返回一个FutureTask,然后执行FutureTask.get()方法或FutureTask.cancel()方法。
FutureTask有三种状态:未启动(FutureTask.run()没有被执行之前的状态)、已启动(FutureTask.run()方法执行过程中)、已完成(FutureTask.run()方法执行完成或被取消),这三种状态的流转如下图所示:
FutureTask的实现是基于AQS(AbstractQueuedSynchrouizer)来实现的,之前已经说过,每一个基于AQS实现的同步器都会至少包含一个acquire操作和至少一个release操作。AQS被作为模板方法模式的基础类提供给FutureTask的内部子类Sync实现了AQS的tryAcquireShared(int)方法和tryReleaseShared(int)方法,Sync通过这两个方法来检查和更新同步状态。
FutureTask涉及示意图如下图所示:
如上图所示,FutureTask.get()方法会调用AQS的acquireSharedInterruptibly(int)方法,该方法首先会回调在子类Sync中的tryAcquireShared()方法来判断acquire操作是否成功(state状态状态是否为执行完成RAN或取消状态CANCELED&runner不为null),如果成功则get()方法立刻返回,如果失败则到线程等待队列中去等待其他线程执行release操作;当其他线程执行release操作(比如FutureTask.run()或FutureTask.cancel())唤醒当前线程后,当前线程再次执行tryAcquireShared()将返回正值1,当前线程将离开线程等待队列并唤醒它的后继线程。
Run方法执行过程如下:
执行在构造函数中指定的任务(Callable.call()),然后以原子方式来更新状态(调用AQS.compareAndSetState(int expect, int update),设置state的状态为RAN),如果这个原子操作成功,就设置代表计算结果的变量result的值为Callable.call()的返回值,然后调用AQS.release(int)。
AQS.rease首先会调用子类Sync中实现的tryReleaseShared方法来执行release操作(设置运行任务的线程为null,然后返回false),然后唤醒等待队列中的第一个线程。
最后调用Future.done()方法。