Java线程池的那些事儿(1)

​Java中开启一个线程执行任务的时候,走的是下面的流程:

new -> run -> destroy

但在真实的生产环境中需要大量线程来执行任务的时候,线程的创建和销毁是比较昂贵的资源消耗,会消耗大量的cpu和内存,这是其一。也有可能线程创建和销毁花的时间比真正线程用来执行任务花的时间还要长,会大大影响效率,这是其二。

 

为了解决上述问题,达到一个可以让创建的线程复用的目的,并发大师Doug Lea 为我们提供了一套 Executor 框架,本质上也就是我们常说的线程池。

 

线程池的优点(合理使用的前提下):

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

  • 提高响应速度。 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能 立即执行。    

  • 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

  • 实现某些与时间相关的功能,如定时执行、周期执行等。  

   

 

  

以上成员均在java.util.concurrent包中,是JDK并发包的核心类。Executor框架中我们最主要关心的就是ThreadPoolExecutorExecutors

 

ThreadPoolExecutor:是线程池的实现类,也是Executor框架中最核心的类。

 

Executors:类则扮演着线程池工厂的角色,里面提供了好多静态方法,通过Executors可以取得一个拥有特定功能的线程池。

 

 我们来看ThreadPoolExecutor

 

1.线程池的状态 

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

 

代码中成员变量ctl是一个Integer的原子变量,高3位用来表示线程池状态,后面29位用来记录线程池线程个数,我们联想到ReentrantReadWriteLock(高16位记录读锁,低16位记录写锁)中也是使用一个变量来保存两种信息。

 

线程池的状态和含义如下:

  • RUNNING:接受新任务并且处理阻塞队列里的任务。

  • SHUTDOWN:拒绝新任务但是处理阻塞队列里的任务。

  • STOP:拒绝新任务并且抛弃阻塞队列里的任务,同时会中断正在处理的任务。

  • TIDYING:所有任务都执行完(包含阻塞队列里面的任务)后当前线程池活动线程数为0,将要调用terminated方法。

  • TERMINATED:终止状态。terminated方法调用完成以后的状态。线程池状态转换列举如下。

  • RUNNING -> SHUTDOWN :显式调用shutdown()方法,或者隐式调用了finalize()方法里面的shutdown()方法。

  • RUNNING或SHUTDOWN)-> STOP :显式调用shutdownNow()方法时。

  • SHUTDOWN -> TIDYING :当线程池和任务队列都为空时。

  • STOP -> TIDYING :当线程池为空时。

  • TIDYING -> TERMINATED:当terminated()hook方法执行完成时。

 

  2.线程池的创建: 

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

线程池中各个参数的含义:

  • corePoolSize:线程池核心线程个数,也就是线程池的基本大小。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

  •  keeyAliveTime:存活时间。如果当前线程池中的线程数量比核心线程数量多,并且是闲置状态,则表示这些闲置的线程能存活的最大时间。

  • TimeUnit:存活时间的时间单位。

  • workQueue:用于保存等待执行的任务的阻塞队列(注意不是工作队列)

  • maximunPoolSize:线程池最大线程数量。线程池中允许创建的最大线程数个数。如果队列(workQueue)满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果

  • ThreadFactory:创建线程的工厂(我们可以做相应的定制化,比如给线程设置一个name)。

  • RejectedExecutionHandler:饱和策略,当队列满并且线程个数达到maximunPoolSize后采取的策略来拒绝新提交的任务。

     

workQueue的种类:

  1. ArrayBlockingQueue 基于数组的有界队列,按FIFO(先进先出)原则对元素进行排序。

  2. LinkedBlockingQueue基于链表的无界队列,按FIFO排序元素,一般来说吞吐量要高于ArrayBlockingQueue。

  3. SynchronousQueue 最多只有一个元素的同步队列,一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直是阻塞状态。

  4. PriorityBlockingQueue 优先级队列,一个支持优先级的无界阻塞队列,直到系统资源耗尽,默认情况下元素采用自然顺序升序排列。

 

RejectedExecutionHandler拒绝策略:

  1. AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。

  2. CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

  3. DiscardOldestPolicy策略:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。

  4. DiscardPolicy策略:该策略默默地丢弃无法处理的任务,不予任何处理。

     

在实际的开发中除了上述的4种饱和策略以外,也可以根据应用场景需要来实现  RejectedExecutionHandler接口自定义饱和策略,比如记录相对应的日志。

 

3.线程池的执行流程:

我们可以使用execute()和submit()方法来提交任务,execute()方法用于提交不需要返回值的任务,submit()方法用于提交需要返回值的任务。

 

下面来具体了解线程池的执行流程:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();        
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

 

 

 

                                    线程池执行流程

 

结合代码和上图可以很清晰的看出线程池的执行流程:

  1. 线程池判断核心线程池里的线程是否都在执行任务。如果没有,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则进入下个流程。

  2. 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。

  3. 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。

 

4.线程池的关闭:

 

public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }
  
public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

 

线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

  • shutdown 将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

  • shutdownNow 首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

 

5.线程池的监控和扩展

 

如果在系统中大量使用线程池,则有必要对线程池进行监控,方便在出现问题时,可以根据线程池的使用状况快速定位问题。可以通过线程池提供的参数进行监控,在监控线程池的时候可以使用以下属性:

  •  getCorePoolSize():该方法返回一个表示线程池核心线程数的整型数值。该值表示维持内部线程池所需要的最少线程数,尽管此时可能没有任何任务在线程池中执行。

  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。

  •  largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。

  • getActiveCount():该方法返回一个整型数值,它表示当前正在执行任务的线程数。

  • getTaskCount():该方法返回一个长整型数值,它表示当前已安排执行计划的任务数。

  • getCompletedTaskCount():该方法返回一个长整型数值,它表示当前已经执行并且已经完成执行计划的任务数。

 

ThreadPoolExecutor还提供了beforeExecute()、afterExecute()和terminated()三个接口在任务执行前、执行后和线程池关闭前执行一些代码来对线程池进行监控。

 

我们了解了ThreadPoolExecutor之后,来看看JDK中的Executors工具类为我们提供了哪些线程池。

 

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

可重用固定线程数的线程池,corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads,FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。

这里要注意:由于使用了无界队列,

  1. maximumPoolSize和keepAliveTime将是两个无效参数

  2. 运行中的FixedThreadPool不会拒绝任务

 

SingleThreadExecutor:

 

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

创建一个核心线程个数和最大线程个数都为1的线程池,其他参数与FixedThreadPool相同。SingleThreadExecutor也是使用无界队列LinkedBlockingQueue作为线程池的工作队列,所以对线程池带来的影响和FixedThreadPool是一样的,不在赘述。

 

CachedThreadPool :

 

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

创建一个按需创建线程的线程池,初始线程个数为0,最多线程个数为Integer.MAX_VALUE,并且阻塞队列为SynchronousQueue同步队列。keeyAliveTime=60说明只要当前线程在60s内空闲则回收。这个队列的特殊之处在于,加入同步队列的任务会被马上执行,同步队列里面最多只有一个任务。

这里要注意的是CachedThreadPool的maximumPool是无界的,这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程从而消耗大量的cpu资源。

最后来看看阿里对于线程池使用的一些建议和规范:

 

 

总结

本文详细介绍了Executor框架里面的ThreadPoolExecutor和Executors,线程

的优点以及使用线程池需要注意的一些细节。关于线程池还有许多的内容和知识点,后续会逐步更新。

 

写在最后

感谢阅读,文中有错误的地方,欢迎指出,也欢迎关注微信公众号一起交流

    

                                                                   

 


参考:

《实战Java高并发程序设计 》  

《Java 并发编程之美》   

 阿里巴巴Java开发手册           

 

 

  

  

  

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值