线程池之如何动态调节线程个数和线程池是如何处理抛出异常的线程

一、线程池是如何动态调节线程个数

为什么要动态调节线程的个数呢?

比方说我们公司在晚上7点的时候有免费的加班餐,大家可以在App上使用企业支付来免费吃饭,所以会有一个高峰期,这时候就可以把线程池的线程数提高,9点之后再降下来。

我之前分析了线程池的 回收、执行等源码,大家可以先去看看:

手把手教你线程池源码分析

如何调节呢?
可以调用线程池的 的 如下方法:

// 设置核心线程大小
executorService.setCorePoolSize(10);
// 设置最大线程大小,为什么要设置这个呢?下面会分析
executorService.setMaximumPoolSize(10);

我们先举个例子看看结果:

 ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
            for (int i = 0; i < 15; i++) {
    
    
                int a = i;
                Runnable runnable = () -> {
    
    
                    Log.e("gggg", "任务" + (a + 1) + "开始");
                    try {
    
    
                        Thread.sleep(3_000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    Log.e("gggg", "任务" + (a + 1) + "结束");
                };
                executorService.execute(runnable);
            }
            executorService.shutdown();

这是没有动态调节的,我们分析下需要多长时间?
我们 提交 15 个任务,每个任务耗时3秒,核心线程是 2 个,最大线程数 5个,队列大小是10, 注意 ArrayBlockingQueue 是不会扩容的,超过初始容量直接抛出异常。

分析如下:

● 首先 2个核心线程执行2个任务,

● 再次,将10个任务放入任务队列

● 然后,非核心线程3个执行3个任务

即 开始有5个线程执行5个任务,耗时3秒;

任何这5个任务执行完成,那么这5个线程回去任务队列中取5个任务执行,又耗时3秒,此时任务队列中还有5条任务;

等这5个任务执行完成的时候,这5个线程去取任务队列中的最后5个任务,又耗时3秒,可以看出这15个任务共耗时9秒。

我们看看log是不是9秒:

在这里插入图片描述

从log中可以看出,差不多就是9秒啦。

接下来动态调节了线程数了

   ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
            for (int i = 0; i < 15; i++) {
    
    
                int a = i;
                Runnable runnable = () -> {
    
    
                    Log.e("gggg", "任务" + (a + 1) + "开始");
                    try {
    
    
                        Thread.sleep(3_000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    Log.e("gggg", "任务" + (a + 1) + "结束");
                };
                executorService.execute(runnable);
            }
            executorService.shutdown();
            // 动态调节线程池中核心线程为10,最大线程为10
            executorService.setMaximumPoolSize(10);
            executorService.setCorePoolSize(10);

同样,我们分析下:

开始我们有5个线程执行5个任务,后来我们增加了5个线程,那么新增加的5个线程就去任务队列中取5条任务执行,3秒后,这10个任务执行完了,然后这10个线程又去任务队列中取任务,此时只有5个线程能取到任务并执行,又耗时3秒,所以总共耗时6秒。

我们看看结果啊:
在这里插入图片描述
可以看到 log上看,总共花了6秒

可以看出动态调节线程大小的效果还是很理想的,这只是个简单的例子,仅供说明,参考。

下面来结合源码分析为什么可行?

先看 setCorePoolSize 方法:

    public void setCorePoolSize(int corePoolSize) {
    
    
        if (corePoolSize < 0)
            throw new IllegalArgumentException();
         // 设置的核心线程数减去最开始设置的核心线程数   
        int delta = corePoolSize - this.corePoolSize;
        // 重新设置核心线程数
        this.corePoolSize = corePoolSize;
        // 线程池中已有的线程数大于你设置的核心线程数,那么中断空闲的线程任务
        // 这是什么场景呢?就是你高峰期过后,降低线程数,
        // 所以线程池中的已有线程大于你设置的线程数
        if (workerCountOf(ctl.get()) > corePoolSize)
            interruptIdleWorkers();
        else if (delta > 0) {
    
     // 如果设置的核心线程数大于之前设置的
            // k 取队列中任务数和delta的最小值,为什么?
            // 因为你调节线程数的目的就是为了添加线程来执行任务,
           // 比方说,你之前核心线程数是2,现在设置成5,那么delta = 3,
           // 如果,此时任务队列中只有2个任务了,那就完全没必要 添加3个线程去执行任务
            int k = Math.min(delta, workQueue.size());
            // 调用 addWorker(null, true),来创建新线程执行任务,addWorker的作用看之前的文章哈
            while (k-- > 0 && addWorker(null, true)) {
    
    
            // 判断任务队列中的消息是否为空,空了则跳出循环不再添加线程去执行任务。
                if (workQueue.isEmpty())
                    break;
            }
        }
    }

上面从源码分析了动态调节 核心线程数,那为什么同时要设置最大线程数呢 setMaximumPoolSize? 这个其实想想也就知道了,因为线程池中核心线程不能大于最大线程总数啊。这个逻辑体现在哪里呢?其实就是上面 的 addWorker(null, true)

addWorker 部分代码如下:

    private boolean addWorker(Runnable firstTask, boolean core) {
    
    
        retry:
        for (; ; ) {
    
    
          ......
          ......
        Worker w = null;
        try {
    
    
        // new 一个执行任务的 Worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
    
    
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                ......
                ......
                if (workerAdded) {
    
    
                // 调用线程执行任务
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
    
    
            if (!workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

addWorker 中会 new 一个 Worker 去执行任务,Worker 会调用线程工厂创建一个线程来执行任务,Worker 如下:

 Worker(Runnable firstTask) {
    
    
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
 }

任务的执行方法 runWorker(Worker w) 的 部分代码如下:

  ...
  while (task != null || (task = getTask()) != null) {
    
    
  ...
  }
  ...

因为我们调节核心线程数的方法是 : addWorker(null, true),即传的是个空任务,所以 runWorker中的 while 循环直接就去任务队列中取任务 getTask 执行,也就是在 getTask 中判断当前线程池的线程数是否大于最大线程数的,如下:

private Runnable getTask() {
    
    
        boolean timedOut = false; 

        for (; ; ) {
    
    
            int c = ctl.get();
            int rs = runStateOf(c);          
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
    
     
                decrementWorkerCount();             
                return null;
            }

            int wc = workerCountOf(c);

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

      // wc表示的是线程池中已有的线程数量,如果大于最大线程数maximumPoolSize 
      // 那么 wc 肯定是大于1的(因为maximumPoolSize 至少也是1吧),
     // 那么会 把当前线程池中已有线程数减1,然后返回 null,
     // 即通过动态调节核心线程数来执行任务也就没用了,所以同时要调用设置最大线程数的方法
            if ((wc > maximumPoolSize || (timed && timedOut))
                    && (wc > 1 || workQueue.isEmpty())) {
    
    //注3
                if (compareAndDecrementWorkerCount(c))//注4
                    return null;
                continue;
            }         
            try {
    
    
                Runnable r = timed ?
                        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                        workQueue.take();//注5
                if (r != null)
                    return r;
                timedOut = true;//注6
            } catch (InterruptedException retry) {
    
    
                timedOut = false;
            }
        }
    }

我在上面的注释中解释了,为什么要同时设置最大线程数。 设置最大线程数的方法如下:

public void setMaximumPoolSize(int maximumPoolSize) {
    
    
      if (maximumPoolSize <= 0 || maximumPoolSize < corePoolSize)
          throw new IllegalArgumentException();
      this.maximumPoolSize = maximumPoolSize;
      // 线程池中已有的线程数大于最大线程数就中断空闲的线程,这个也是调低最大线程数才会出现
      if (workerCountOf(ctl.get()) > maximumPoolSize)
          interruptIdleWorkers();
  }

好了,动态调节的内容说完了,有问题可以留言哈。下面讲述下 线程池是怎么处理抛出异常任务的线程的

线程抛出异常线程池是如何处理的?会影响其他执行任务的线程吗?线程池是回收还是删除这个线程呢?

我们带着这些疑问,写个demo试试就知道了。代码如下:

  ThreadPoolExecutor executorService = new ThreadPoolExecutor(2, 2, 100, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(2));
        Runnable runnable1 = () -> {
    
    
            System.out.println("任务1开始  threadNmae = " + Thread.currentThread().getName());
            try {
    
    
                Thread.sleep(3_000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("任务1结束 threadNmae = " + Thread.currentThread().getName());
        };

        Runnable runnable2 = () -> {
    
    
            System.out.println("异常任务2  start threadNmae = " + Thread.currentThread().getName());
            throw new RuntimeException("我又出bug啦,哈哈哈!!!!");
        };

        Runnable runnable3 = () -> {
    
    
            System.out.println("任务3开始  threadNmae = " + Thread.currentThread().getName());
            try {
    
    
                Thread.sleep(3_000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("任务3结束 threadNmae = " + Thread.currentThread().getName());
        };
        Runnable runnable4 = () -> {
    
    
            System.out.println("任务4开始  threadNmae = " + Thread.currentThread().getName());
            try {
    
    
                Thread.sleep(3_000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("任务4结束 threadNmae = " + Thread.currentThread().getName());
        };

        executorService.execute(runnable1);
        executorService.execute(runnable2);
        executorService.execute(runnable3);
        executorService.execute(runnable4);
        executorService.shutdown();

我在任务2的时候抛出了一个异常,结果如下:
在这里插入图片描述

从运行结果看来 任务2抛出异常,打印错误堆栈信息,且不会影响其他执行任务的线程。
那么 线程池是怎么处理抛出异常的线程呢? 这个还得从源码中分析:

   final void runWorker(Worker w) {
    
    
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
    
    
            // 如果task你为空,或者去阻塞队列中去取任务不为空,这里的getTask 如果阻塞队列中任务为空 会阻塞当前线程
            // 这里就是线程复用的核心,比方说当这个程执行完当前任务后,就去队列中取任务来执行,这就完成了线程的复用
            while (task != null || (task = getTask()) != null) {
    
    
                w.lock();
              
                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) {
    
    
                    //  这这里会 catch 住运行时的异常且抛出异常                   
                        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 {
    
    
    // 最后会执行到这里,由于是抛出异常了,这里 completedAbruptly = true
            processWorkerExit(w, completedAbruptly);
        }
    }

然后 看看 processWorkerExit(w, completedAbruptly),如下:

   private void processWorkerExit(Worker w, boolean completedAbruptly) {
    
    

       // 如果是线程执行的任务抛出异常,那么会把线程池中线程数减1
        if (completedAbruptly) {
    
    
            System.out.println("任务抛出异常,线程数减1");
            decrementWorkerCount();
        }
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            completedTaskCount += w.completedTasks;
            workers.remove(w); // 移除任务
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
        int c = ctl.get();
//        System.out.println("processWorkerExit" + " c = " + c);
        if (runStateLessThan(c, STOP)) {
    
    
            if (!completedAbruptly) {
    
    
                int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                if (min == 0 && !workQueue.isEmpty())
                    min = 1;
                 // 在核心线程不设置销毁时间时,如果工作线程小于核心线程数的时候那么此条件不成立,执行 下面的addWorker(null, false)来开启新的线程来处理任务
                // 在调用shutDown方法后,会回收核心线程,此时线程池中线程数可能小于核心线程数,或者动态调小核心线程数
                if (workerCountOf(c) >= min)
                    return; // replacement not needed
            }  
              // 这行代码什么时候会走到呢?1: 线程执行的任务抛出异常
              // 2. 如上面的解释 线程池中的线程小于核心线程
            if (completedAbruptly){
    
    
                System.out.println("任务抛出异常, 添加一个执行任务的线程");
                addWorker(null, false);
            }
        }
    }

这上面的代码就分析了,任务抛出异常线程池是如何处理的。

现在我们回答之前的问题:

● 有任务抛出异常,那么会答应错误堆栈信息

● 不会影响其他的任务

● 删除抛出异常的线程,重新添加一个线程去执行任务

以后文章会同步到个人公众号,欢迎关注交流

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/zhujiangtaotaise/article/details/111597876