线程池的实现原理

线程池的优势

一、线程是稀缺资源,使用线程池能够减小建立和销毁线程的次数,每一个工做线程均可以重复使用。 设置线程的个数请看极客时间上这边文章的介绍:time.geekbang.org/column/arti…, cpu密集型:cpu核数+1;IO密集型:单核:1 +(I/O 耗时 / CPU 耗时),多核:CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]数组

二、能够根据系统的承受能力,调整线程池中工做线程的数量,防止由于消耗过多内存致使服务器崩溃。缓存

线程池的建立

public ThreadPoolExecutor(int corePoolSize,
                                int maximumPoolSize,
                                long keepAliveTime,
                                TimeUnit unit,
                                BlockingQueue<Runnable> workQueue,
                                RejectedExecutionHandler handler) 
复制代码
corePoolSize:线程池核心线程数量

maximumPoolSize:线程池最大线程数量

keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间

unit:存活时间的单位

workQueue:存听任务的队列

handler:超出线程范围和队列容量的任务的处理程序

注:关于workQueue参数的取值,JDK提供了4种阻塞队列类型供选择:
            ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
            
            inkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量一般要高于ArrayBlockingQuene 

            SynchronousQuene:一个不存储元素的阻塞队列,每一个插入操做必须等到另外一个线程调用移除操做,不然插入操做一直处于阻塞状态,吞吐量一般要高于ArrayBlockingQuene;

            PriorityBlockingQuene:具备优先级的无界阻塞队列;

     threadFactory:线程工厂,主要用来建立线程;

     handler:表示当拒绝处理任务时的策略,有如下四种取值

 注: 当线程池的饱和策略,当阻塞队列满了,且没有空闲的工做线程,若是继续提交任务,必须采起一种策略处理该任务,线程池提供了4种策略:

        ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

        ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,可是不抛出异常。

        ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,而后从新尝试执行任务(重复此过程)

        ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

        固然也能够根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。


复制代码

线程池的实现原理

提交一个任务到线程池中,线程池的处理流程以下:

一、判断线程池里的核心线程是否都在执行任务,若是不是(核心线程空闲或者还有核心线程没有被建立)则建立一个新的工做线程来执行任务。若是核心线程都在执行任务,则进入下个流程。

二、线程池判断工做队列是否已满,若是工做队列没有满,则将新提交的任务存储在这个工做队列里。若是工做队列满了,则进入下个流程。

三、判断线程池里的线程是否都处于工做状态,若是没有,则建立一个新的工做线程来执行任务。若是已经满了,则交给饱和策略来处理这个任务。
复制代码

线程池的源码解读

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
            //若是线程数大于等于基本线程数或者线程建立失败,将任务加入队列
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
           //线程池处于运行状态而且加入队列成功
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
           //线程池不处于运行状态或者加入队列失败,则建立线程(建立的是非核心线程)
            else if (!addIfUnderMaximumPoolSize(command))
          //建立线程失败,则采起阻塞处理的方式
                reject(command); // is shutdown or saturated
        }
    }
复制代码

初始化四种类型的线程池

一、newFixedThreadPool()

说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene做为阻塞队列
特色:即便当线程池没有可执行任务时,也不会释放线程。
复制代码

二、newCachedThreadPool()

说明:初始化一个能够缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue做为阻塞队列;
特色:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,若是没有空闲线程,则建立新线程执行任务,会致使必定的系统开销;
所以,使用时要注意控制并发的任务数,防止因建立大量的线程致使而下降性能。
复制代码

三、newSingleThreadExecutor()

说明:初始化只有一个线程的线程池,内部使用LinkedBlockingQueue做为阻塞队列。
特色:若是该线程异常结束,会从新建立一个新的线程继续执行任务,惟一的线程能够保证所提交任务的顺序执行
复制代码

四、newScheduledThreadPool()

特定:初始化的线程池能够在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可使用该线程池按期的同步数据。

总结:除了newScheduledThreadPool的内部实现特殊一点以外,其它线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。
复制代码

向线程池提交任务

有两种方式:bash

Executor.execute(Runnable command);

  ExecutorService.submit(Callable<T> task);
复制代码

execute()的内部实现

1.首次经过workCountof()获知当前线程池中的线程数,

  若是小于corePoolSize, 就经过addWorker()建立线程并执行该任务;

&emsp;不然,将该任务放入阻塞队列;

2. 若是能成功将任务放入阻塞队列中,  

若是当前线程池是非RUNNING状态,则将该任务从阻塞队列中移除,而后执行reject()处理该任务;

若是当前线程池处于RUNNING状态,则须要再次检查线程池(由于可能在上次检查后,有线程资源被释放),是否有空闲的线程;若是有则执行该任务;

三、若是不能将任务放入阻塞队列中,说明阻塞队列已满;那么将经过addWoker()尝试建立一个新的线程去执行这个任务;若是addWoker()执行失败,说明线程池中线程数达到maxPoolSize,则执行reject()处理任务;
复制代码

sumbit()内部实现

会将提交的Callable任务会被封装成了一个FutureTask对象

FutureTask类实现了Runnable接口,这样就能够经过Executor.execute()提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法; 

比较:

 两个方法均可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法能够返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。 
复制代码

线程生命周期

当线程被建立并启动后, 并非一启动就进入执行状态,也不是一直处于执行状态.在线程的生命周期中,要通过新建、就绪、运行、阻塞、死亡五种状态.服务器

新建状态

当程序使用new关键字建立了一个线程以后,该线程就处于新建状态,此时仅由JVMJ为其分配内存,并初始化其成员变量的值.并发

就绪状态

当线程对象调用了 start()方法以后,该线程处于就绪状态。Java 虚拟机会为其建立方法调用栈和 程序计数器,等待调度运行。性能

运行状态

若是处于就绪状态的线程得到了cpu,开始执行run()方法执行体,则该线程处于运行状态.ui

阻塞状态

阻塞状态是指线程由于某种缘由放弃了cpu使用权,也即让出了cpu时间片,暂停中止运行.直到线程进入可运行状态,才有机会再次得到cpu时间片转到运行状态. 线程状态之间到转换spa

相关文章
相关标签/搜索