Java线程池以及线程池监控(I)

Java并发学习 专栏收录该内容
6 篇文章 0 订阅

本文部分来源自
Java线程池实现原理及其在美团业务中的实践

线程池是什么

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

当然,使用线程池可以带来一系列好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池解决的问题是什么

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  • 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  • 系统无法合理管理内部的资源分布,会降低系统的稳定性。
  • 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

“池化”思想不仅仅能应用在计算机领域,在金融、设备、人员管理、工作管理等领域也有相关的应用。

在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。通过共享资源,使用户在低投入中获益。除去线程池,还有其他比较典型的几种使用策略包括:

  • 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  • 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
  • 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。

Java中的线程池

Java中的线程池是由ThreadPoolExecutor 来实现,继承关系如下图
在这里插入图片描述
ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增加了一些能力:
(1)扩充执行任务的能力,补充可以为一个或一批异步任务生成Future的方法;
(2)提供了管控线程池的方法,比如停止线程池的运行。
AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。比如AbstractExecutorService中的submit方法

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor介绍

线程池的构造

ThreadPoolExecutor的构造方法有七个参数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  • corePollSize:核心线程池,当核心线程池没有满的时候,每来一个任务都会创建一个线程
  • maximumPoolSize:最大线程池数,当线程池活跃线程数等于最大线程数,下一个任务来,不会创建线程去执行任务,而是执行拒绝策略
  • keepAliveTime:线程的存活时间,当线程执行完任务后会存活keepAliveTime的时间,看看还有没有任务要执行。如果没有线程就会被回收
  • unit:keepAliveTime的时间单位,秒,毫秒等
  • workQueue:阻塞队列,管理任务的地方,当核心线程池满了,后面的任务会进入到workQueue进行等待
    常用的阻塞队列有
    public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

可以看到ArrayBlockingQueue是有界阻塞队列,而LinkedBlockingQueue不传参数的话,默认大小是Integer.MAX_VALUE可以视为是无限的阻塞队列

  • threadFactory:线程工厂,创建线程的地方,不传的话,默认是defaultThreadFactory
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
    public static ThreadFactory defaultThreadFactory() {
        return new DefaultThreadFactory();
    }

我们可以通过实现ThreadFactory接口,来自定义线程工厂,这样就可以按照自己的需要定义线程池的名字,再把自定义的线程工厂传入构造参数中,这样线程池创建线程时,就会从我们的工厂创建线程

    static class CustomThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            //自定义线程池名称前缀
            namePrefix = "customPool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
  • handler:拒绝策略,当超过最大线程数时,任务提交就会被拒绝,从而执行拒绝策略,线程池中有四种拒绝策略
  1. CallerRunsPolicy:提交任务的线程来执行该任务
    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
  1. AbortPolicy(默认):直接返回异常,该策略是默认的拒绝策略
    public static class AbortPolicy implements RejectedExecutionHandler {

        public AbortPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }
  1. DiscardPolicy:什么都不做
    public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
  1. DiscardOldestPolicy:丢弃最老的任务,然后线程executor任务
    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

可以看见上面四种策略都是实现了RejectedExecutionHandler这个接口,所以我们也可以通过实现该接口,来自定义符合我们要去的拒绝策略,比如当线程满的时候,任务一直等待队列有位置把任务放进队列

    public static class CustomRejectHandle implements RejectedExecutionHandler{
        
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            if (!executor.isShutdown()){
                try {
                    executor.getQueue().put(r);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    } 

线程池的生命周期

线程池创建出来了,接下来说一说线程池的生命周期
在线程池内部,通过一个变量 ctl来维护运行状态(runState)和线程数量(workerCount),在线程池内部,runState是通过高三位表示,而workerCount是低29位表示

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

    // runState is stored in the high-order bits
    //11100000 00000000 00000000 00000000
    private static final int RUNNING    = -1 << COUNT_BITS;
    //00000000 00000000 00000000 00000000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    //00100000 00000000 00000000 00000000
    private static final int STOP       =  1 << COUNT_BITS;
    //01000000 00000000 00000000 00000000
    private static final int TIDYING    =  2 << COUNT_BITS;
    //01100000 00000000 00000000 00000000
    private static final int TERMINATED =  3 << COUNT_BITS;

    // ~CAPACITY:11100000000000000000000000000000
    //计算当前的运行状态
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    //计算当前的线程数量
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    通过状态和线程数生成ctl
    private static int ctlOf(int rs, int wc) { return rs | wc; }

可以看到线程池有五种状态,2的三次方能表示八种状态,所以用高三位来表示运行状态,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。
在这里插入图片描述
线程池的生命周期的转换:
在这里插入图片描述

线程池的运行机制

线程池创建完成后,通过execute方法就能够执行一个任务,我们来看一看execute内部都做了什么事

   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.从源码可以看出execute先是对任务进行判空,不能传空任务进来,接着会去判断当前的线程数量是否小于corePoolSize,如果小于则创建线程去执行该任务并直接返回,如果创建失败则拿到当前的ctl,个人认为会失败的原因是execute是没有加锁的,可能workerCountOf(c) < corePoolSize满足,但是当执行到**addWorker(command, true)**时,其他线程已经把核心线程池加满了,导致再创建核心线程池的线程失败。
2. 如果当前线程数大于等于corePoolSize或者创建失败,则继续往下判断,如果当前的线程池状态是RUNNING,就把任务添加到阻塞队列中,offer是不阻塞的即队列满了就返回false,如果添加成功了,还需要再次检查线程池的状态,如果线程池被关闭了,就把刚刚添加到队列的任务移除,并执行拒绝策略。如果是RUNNING状态但是当前没有线程了,就会创建一个非核心线程池的线程,为的就是刚刚添加到队列的任务能够被执行,同时避免corePoolSize为0的情况。
3.如果添加队列失败,就会创建非核心线程池的线程去执行任务,如果创建失败,执行拒绝策略。
简单的用流程图表示
在这里插入图片描述

  • 1
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值