【小墨java】java基础之---线程池ThreadPoolExecutor原理解析

一,前言

                                                            雷死人不偿命的动态搞笑GIF图(1024)

 

大家好,我是小墨,这期介绍线程池。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。

在创建线程池Executors类中有四种建立线程池的方法,分别为:

  1. newSingleThreadPool:创建一个单线程的线程池,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。
  2. newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变
  3. newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。
  4. newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求

但我们注意底层其实创建线程池是使用ThreadPoolExecutor类来进行线程池的操作,我们先简单介绍ThreadPoolExecutor各个参数,然后来说下其底层代码如何实现

二,ThreadPoolExecutor使用介绍

我们先来一段简单代码看下如何使用,我们看到有几个具体参数设置,比较重要。

​
 public static void main(String[] args) throws ExecutionException, InterruptedException {

         ThreadPoolExecutor excutors = new ThreadPoolExecutor(
                                GatewayConfig.HTTP_SERVER.PRE_DISPATCHER_THREAD_SIZE / 3,
                                GatewayConfig.HTTP_SERVER.PRE_DISPATCHER_THREAD_SIZE,
                                3, TimeUnit.MINUTES,
                                new LinkedBlockingDeque<Runnable>(GatewayConfig.HTTP_SERVER.QUEUE_LENGTH),
                                new ThreadFactory() {
                                    private volatile int index = 1;

                                    @Override
                                    public Thread newThread(Runnable r) {
                                        Thread thread = new Thread(r);
                                        index++;
                                        thread.setName("dispatch_worker_" + index);
                                        return thread;
                                    }
                                }
                        );

                        excutors.setRejectedExecutionHandler(new RejectedExecutionHandler() {
                            @Override
                            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                if (r instanceof Rejectable) {
                                    Rejectable reject = (Rejectable) r;
                                    reject.reject();

                                }
                            }
                        });

        for (int i = 0; i < 5; i++) {
            executor.submit(new DivTask(100, i));
        }

    }
    
    static class DivTask implements Runnable {
        int a, b;
        public DivTask(int a, int b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public void run() {
            double re = a / b;
            System.out.println(re);
        }
    }

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

参数分别为:

corePoolSize 核心线程池数

  • 核心线程会一直存活,即使没有任务需要执行
  • 当设置此值时,初始化线程池时会创建一定数量的线程放入线程池中,后面线程需要时可以直接调用,而不需要再创建,设置为0则不会预先创建线程。

 

maximumPoolSize 线程池最大容量

  • 当线程数>corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务,直到线程数量达到maxPoolSize
  • 当线程数已经=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

 

keepAliveTime 线程池空闲时,线程存活的时间

  • 当线程空闲时间达到keepAliveTime时,线程会被销毁,直到线程数量=corePoolSize

TimeUnit 时间单位

ThreadFactory 线程工厂:可以设置线程创建名字等。

BlockingQueue任务队列:提交的任务会放置入任务队列中,进行排队,任务队列包括无限队列和有限队列。

RejectedExecutionHandler 线程拒绝策略,

两种情况会拒绝处理任务:
1、当线程数已经达到maxPoolSize,且任务队列已满时,会拒绝新任务
2、当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务(并不是立马停止,而是执行完再停止)。
若拒绝后,此时,线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置,默认值是AbortPolicy,会抛出异常
hreadPoolExecutor类有几个内部实现类来处理这类情况:
1: AbortPolicy 丢弃任务,抛运行时异常
2:CallerRunsPolicy 执行任务(这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功) 如果执行器已关闭,则丢弃.
3:DiscardPolicy 对拒绝任务直接无声抛弃,没有异常信息
4:DiscardOldestPolicy 对拒绝任务不抛弃,而是抛弃队列里面等待最久的(队列头部的任务将被删除)一个线程,然后把拒绝任务加到队列(Queue是先进先出的任务调度算法,具体策略会咋下面有分析)(如果再次失败,则重复此过程)
5:实现RejectedExecutionHandler接口,可自定义处理器(可以自己实现然后set进去)

 

整个流程可以参考下图,概括如下

当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务

我们常用的向线程池提交线程的方式有execute,submit,我们看下区别:

  • execute提交的方式只能提交一个Runnable的对象,且该方法的返回值是void,无返回值
  • submit() 的返回值 Future 调用get方法时,可以捕获处理异常。

三,源码解析

我们看下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;
    }

我们主要关注下execute方法

1,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. 如果正在工作的线程小于corePoolSize,则会创建线程,然后返回。
  2. 当核心数已经达到corePoolSize时,则会判断是否能够放入阻塞队列,放入成功后,那么这时候可能还没到线程池的maxnum,所以尝试增加一个Worker。
  3. 如果放入不成功,说明达到最大线程池大小,拒绝。

我们看到这里的重点是addWorker方法,我们重点讲下

1)addWorker方法

Worker的增加和Task的获取以及终止都是在此方法中实现的

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker方法看起来很复杂,我们不每行代码一一介绍,我们抽丝剥茧可以得到主要代码如下,逻辑为:

  • 循环中判断状态是否可以加入线程,略过不计
  • 使用当前线程包装成Worker对象,因为使用hashSet<Worker> workers作为集合存放,所以使用可重入锁锁住,只允许一个线程去往workers加。
  • 注意我们要注意到在这里能加入的线程为核心线程。
  • 如果往workers加成功,t.start()启动,由操作系统去调用。
   private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
        //.....
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                       //.....
                        workers.add(w);
                       //....
                    }
                finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

我们重点关注Worker对象,Worker对象实现了Runnable,继承了AbstractQueuedSynchronizer AQS

重写run方法,

 public void run() {
            runWorker(this);
        }

runWorker方法为重点方法值得我们去分析。

2)runWorker方法-----重点关注

我们分析这个代码逻辑如下:

  • 在循环中判断当前线程和阻塞队列中的线程(getTask方法)是否为空,
  • 使用Worker类的lock方法自我加锁,不影响其他线程,也不允许其他线程影响,执行线程的run方法
  • 使用模板模式,提供了beforeExecute,afterExecute给用户进行方法增强
  • 这个线程一直保持,因为阻塞队列的take方法即使在阻塞队列为空也会阻塞,这部分不清楚的请去了解下。
  final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

 

我们注意到getTask()方法,我们也一起放上来,我们关注主要逻辑如下:

  • wc参数标识当前Worker超时是否要退出,我们直到线程池可以设置超时时间,超时则取出最后放进去的线程。判断wc > corePoolSize。如果大于,需要减小空闲的Worker数,那么timed为true,阻塞队列要
    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
    根据超时时间是否到达来移除头结点,但是wc <= corePoolSize时,timed为false,取下最早放入的线程让线程脱离阻塞状态进入就绪状态。
 private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // Are workers subject to culling?
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

2,关闭线程池

对于关闭线程池主要有两个方法shutdown()和shutdownNow():

我们比较下这两个方法区别:

shutdown:

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

判断是否能够操作,设置线程池状态为shutdown,中断所有空闲线程,不允许新的线程进入线程池,我们来看下什么是中断空闲线程。

  中断空闲线程需要先判断是否中断,尝试去获取锁,那么正在运行的线程则获取不到,允许继续运行,而阻塞队列未运行的线程则可以顺利获取到锁,然后启动中断。

     private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

 

shutdownNow():

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;
    }

shutdownNow是调用interruptWorkers,拒绝所有新Task的加入,同时中断所有线程,WorkerQueue中没有执行的线程全部抛弃。所以此时Pool是空的,WorkerQueue也是空的。

 private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }

  void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }

 

四,总结

线程池核心是使用ThreadPoolExecutor类,利用阻塞队列来存储线程池,核心线程直接启动,当发现运行线程数小于核心线程数,从阻塞队列去线程运行。

题外话:

设计线程池数量

对于设置线程池大小其实是个需要综合业务来设计的情况。我们简单的考虑就是:

  • 就是如果你是CPU密集型运算,那么线程数量和CPU核心数相同就好,避免了大量无用的切换线程上下文
  • 如果你是IO密集型的话,需要大量等待,那么线程数可以设置的多一些,比如CPU核心乘以2.

参考文章

https://www.jianshu.com/p/ade771d2c9c0

https://juejin.im/post/6844903600393715719

【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 线程池技术提高系统吞吐量(附带线程池参数详解和使用注意事项)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值