线程池详解

目录

前言

一、线程池的使用场景

1. 加快请求响应(响应时间优先)

2. 加快处理大任务(吞吐量优先)

二、线程池的创建及重要参数

三、线程池中的线程创建流程

四、workQueue队列

五、常见的几种自动创建线程池方式

六、handler的拒绝策略

七、线程池的关闭方式

八、线程池实现线程复用的原理

九、ThreadPoolExecutor#execute 源码分析

十、手动创建线程池(推荐)

十一、Springboot中使用线程池

十二、beforeExecute和afterExecute

 十三、补充

1.Callable和Runnable

2.Future和FutureTask

3.实现优先使用运行线程及调整线程数大小的线程池(线程池的优化)

4.coreSize满了优先创建线程

总结


前言

众所周知,创建线程的方式有以下几种:

1.继承Thread类并重写run()方法,然后调用start()方法启动线程。

2.实现Runnable接口,然后创建Thread对象并将Runnable对象作为参数传递给Thread的构造方法,最后调用start()方法启动线程。

3.实现Callable接口,然后创建FutureTask对象并将Callable对象作为参数传递给FutureTask的构造方法,最后创建Thread对象并将FutureTask对象作为参数传递给Thread的构造方法,最后调用start()方法启动线程。

4.使用线程池创建线程,Java提供了Executor和ExecutorService接口以及ThreadPoolExecutor类来创建线程池。可以通过调用Executor的静态工厂方法来创建线程池。例如,Executors.newFixedThreadPool()方法创建一个固定线程数的线程池。

5.使用ScheduledExecutorService接口创建定时执行任务的线程池,可以通过ScheduledExecutorService的静态工厂方法来创建线程池,例如,Executors.newScheduledThreadPool()方法创建一个定时执行任务的线程池。

其中,使用线程池创建线程是我们日常开发中最常使用的一种方式,因此本篇就着重来介绍并详解一下线程池的基本概念和常见的使用方法。


一、线程池的使用场景

java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

1. 加快请求响应(响应时间优先)

比如用户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给用户,接口逻辑涉及到聚合、级联等查询,从这个角度来看接口返回越快越好,那么就可以使用多线程方式,把聚合/级联查询等任务采用并行方式执行,从而缩短接口响应时间。这种场景下使用线程池的目的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,而是会适当调高corePoolSize和maxPoolSize去尽可能的创造线程来执行任务。

2. 加快处理大任务(吞吐量优先)

比如业务中台每10分钟就调用接口统计每个系统/项目的PV/UV等指标然后写入多个sheet页中返回,这种情况下往往也会使用多线程方式来并行统计。和"时间优先"场景不同,这种场景的关注点不在于尽可能快的返回,而是关注利用有限的资源尽可能的在单位时间内处理更多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时候如果设置了太大的corePoolSize和maxPoolSize可能还会因为线程上下文频繁切换降低任务处理速度,从而导致吞吐量降低。

以上两种使用场景和JVM里的ParallelScavenge和CMS垃圾收集器有较大的类比性,ParallelScavenge垃圾收集器关注点在于达到可观的吞吐量,而CMS垃圾收集器重点关注尽可能缩短GC停顿时间。

二、线程池的创建及重要参数

线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;手动创建体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:

public static ExecutorService newFixedThreadPool(int var0) {
        return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
  }
	
public static ExecutorService newSingleThreadExecutor() {
        return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, 
        TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
  }
 
  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
  }
 
  public static ScheduledExecutorService newScheduledThreadPool(int var0) {
        return new ScheduledThreadPoolExecutor(var0);
 }

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

ThreadPoolExecutor中重要的几个参数详解

  1. corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  2. maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  3. keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  4. unit:keepAliveTime的时间单位
  5. workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
  6. threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  7. handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy
     

三、线程池中的线程创建流程

举个例子:现有一个线程池,corePoolSize=10,maxPoolSize=20,队列长度为100,那么当任务过来会先创建10个核心线程数,接下来进来的任务会进入到队列中直到队列满了,会创建额外的线程来执行任务(最多20个线程),这个时候如果再来任务就会执行拒绝策略。 

线程创建的流程

  1. 当任务提交之后,线程池首先会检查当前线程数,如果当前的线程数小于核心线程数(corePoolSize),比如最开始创建的时候线程数为 0,则新建线程并执行任务。
  2. 当提交的任务不断增加,创建的线程数等于核心线程数(corePoolSize),新增的任务会被添加到 workQueue 任务队列中,等待核心线程执行完当前任务后,重新从 workQueue 中获取任务执行。
  3. 假设任务非常多,达到了 workQueue 的最大容量,但是当前线程数小于最大线程数(maximumPoolSize),线程池会在核心线程数(corePoolSize)的基础上继续创建线程来执行任务。
  4. 假设任务继续增加,线程池的线程数达到最大线程数(maximumPoolSize),如果任务继续增加,这个时候线程池就会采用拒绝策略来拒绝这些任务。

在任务不断增加的过程中,线程池会逐一进行以下 4 个方面的判断

核心线程数(corePoolSize)
任务队列(workQueue)
最大线程数(maximumPoolSize)
拒绝策略

四、workQueue队列

  1. SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
  2. LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
  3. ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务

五、常见的几种自动创建线程池方式

自动创建线程池的几种方式都封装在Executors工具类中:

  1. newFixedThreadPool:使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
  2. newSingleThreadExector:使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
  3. newCachedThreadPool:使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
  4. newScheduledThreadPool:使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致

所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:

在这里插入图片描述

六、handler的拒绝策略

  • AbortPolicy:中断抛出异常
  • DiscardPolicy:默默丢弃任务,不进行任何通知
  • DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
  • CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好)
     

七、线程池的关闭方式

  • shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
  • shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
  • isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

八、线程池实现线程复用的原理

什么是线程复用?

在线程池中,通过同一个线程去执行不同的任务,这就是线程复用。
假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。
示例:

public class FixedThreadPoolDemo {

    static ExecutorService executorService = Executors.newFixedThreadPool(3);

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "-> 
                 执行");
            });
        }
        // 关闭线程池
        executorService.shutdown();
    }

}

执行结果:
pool-1-thread-1-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行
pool-1-thread-3-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行

线程复用的原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来

九、ThreadPoolExecutor#execute 源码分析

java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
        // 如果传入的Runnable的空,就抛出异常
        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);
    }

//如果传入的Runnable的空,就抛出异常 if (command == null) throw new NullPointerException();
execute 方法中通过 if 语句判断 command ,也就是 Runnable 任务是否等于 null,如果为 null 就抛出异常。

if (workerCountOf© < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); }
判断当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程。
addWorker 方法的主要作用是在线程池中创建一个线程并执行传入的任务,如果返回 true 代表添加成功,如果返回 false 代表添加失败。
第一个参数表示传入的任务
第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程(核心线程),大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maximumPoolSize,小于则增加新线程(非核心线程),大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增非核心线程的判断

这一段判断相关源码如下

private boolean addWorker(Runnable firstTask, boolean core) {     
                ...
                int wc = workerCountOf(c);//当前工作线程数
                //判断当前工作线程数>=最大线程数 或者 >=核心线程数(当
                core = true)
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                ...

最核心的就是 core ? corePoolSize : maximumPoolSize 这个三目运算。

// 核心线程已满,但是任务队列未满,添加到队列中
     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);
    }

如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过
if (isRunning© && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就通过 workQueue.offer(command) 将任务放入任务队列中,
任务成功添加到队列以后,再次检查线程池状态,如果线程池不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下:
线程复用源码分析
java.util.concurrent.ThreadPoolExecutor#runWorker
省略掉部分和复用无关的代码之后,代码如下

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // 释放锁 设置work的state=0 允许中断
        boolean completedAbruptly = true;
        try {
            //一直执行 如果task不为空 或者 从队列中获取的task不为空
            while (task != null || (task = getTask()) != null) {
                    task.run();//执行task中的run方法
                }
            }
            completedAbruptly = false;
        } finally {
            //1.将 worker 从数组 workers 里删除掉
            //2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的 
              Worker 进数组 workers
            processWorkerExit(w, completedAbruptly);
        }
    }

可以看到,实现线程复用的逻辑主要在一个不停循环的 while 循环体中。
通过获取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务
直接通过 task.run() 来执行具体的任务(而不是新建线程)
在这里,我们找到了线程复用最终的实现,通过取 Worker 的 firstTask 或者 getTask 方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。

十、手动创建线程池(推荐)

那么上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?对症下药,建立自己的线程工厂类,灵活设置关键参数:
//这里默认拒绝策略为AbortPolicy

private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
 private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());
通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();

十一、Springboot中使用线程池

springboot可以说是非常流行了,下面说说如何在springboot中优雅的使用线程池

/**
 * @ClassName ThreadPoolConfig
 * @Description 配置类中构建线程池实例,方便调用
 * @Author simonsfan
 * @Date 2018/12/20
 * Version  1.0
 */
@Configuration
public class ThreadPoolConfig {
    @Bean(value = "threadPoolInstance")
    public ExecutorService createThreadPoolInstance() {
        //通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程
         名称
         ThreadFactory threadFactory = new 
         ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
         ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, 
         TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), 
         threadFactory, new ThreadPoolExecutor.AbortPolicy());
         return threadPool;
    }


//通过name=threadPoolInstance引用线程池实例
  @Resource(name = "threadPoolInstance")
  private ExecutorService executorService;
 
  @Override
  public void spikeConsumer() {
    //TODO
    executorService.execute(new Runnable() {
    @Override
    public void run() {
      //TODO
     }});
  }

十二、beforeExecute和afterExecute

在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute

protected void beforeExecute(Thread var1, Runnable var2) {

 }

 protected void afterExecute(Runnable var1, Throwable var2) {

 }

这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP,参考下图:

 十三、补充

1.Callable和Runnable

Runnable和Callable都可以理解为任务,里面封装这任务的具体逻辑,用于提交给线程池执行,区别在于Runnable任务执行没有返回值,且Runnable任务逻辑中不能通过throws抛出cheched异常(但是可以try catch),而Callable可以获取到任务的执行结果返回值且抛出checked异常。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
 
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

2.Future和FutureTask

Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:

  1. get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;
  2. get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;
  3. cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;
  4. isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;
  5. isCancelled():判断任务是否被取消;

下面来实际演示Future和FutureTask的用法:

public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        Future<Integer> future = executorService.submit(new Task());
        Integer integer = future.get();
        System.out.println(integer);
        executorService.shutdown();
    }
 
static class Task implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println("子线程开始计算");
            int sum = 0;
            for (int i = 0; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
}

3.实现优先使用运行线程及调整线程数大小的线程池(线程池的优化)

当前在JDK中默认使用的线程池 ThreadPoolExecutor,在具体使用场景中,有以下几个缺点
core线程一般不会timeOut
新任务提交时,如果工作线程数小于 coreSize,会自动先创建线程,即使当前工作线程已经空闲,这样会造成空闲线程浪费
设置的maxSize参数只有在队列满之后,才会生效,而默认情况下容器队列会很大(比如1000)
如一个coreSize为10,maxSize为100,队列长度为1000的线程池,在运行一段时间之后的效果会是以下2个效果:

  • 系统空闲时,线程池中始终保持10个线程不变,有一部分线程在执行任务,另一部分线程一直wait中(即使设置allowCoreThreadTimeOut)
  • 系统繁忙时,线程池中线程仍然为10个,但队列中有还没有执行的任务(不超过1000),存在任务堆积现象

空闲线程优先

空闲线程优先在基本逻辑中,即如果线程数小于coreSize,但如果有空闲线程,就取消创建线程的逻辑. 在有空闲线程的情况下,直接将任务放入队列中,即达到任务执行的目的。
这里的逻辑即是直接调整默认的ThreadPoolExecutor逻辑,通过重载 execute(Runnable) 方法达到效果. 具体代码如下所示:

public void execute(Runnable command) {
    //此处优先处理有活跃线程的情况,避免在<coreSize时,直接创建线程
    if(getActiveCount() < getPoolSize()) {
        if(pool1.offer(command)) {
            return;
        }
    }
    super.execute(command);
}

4.coreSize满了优先创建线程

从之前的逻辑来看,如果放入队列失败,则尝试创建新线程。在这个时候,相应的coreSize肯定已经满了。那么,只需要处理一下逻辑,将其offer调整为false,即可以实现相应的目的。
这里的逻辑,即是重新定义一个BlockingDeque,重载相应的offer方法,相应的参考如下:

public boolean offer(Runnable o) {
    //这里的parent为ThreadPoolExecutor的引用
    int poolSize = parent.getPoolSize();
    int maxPoolSize = parent.getMaximumPoolSize();
 
    //还没到最大值,先创建线程
    if(poolSize < maxPoolSize) {
        return false;
    }
 
    //默认逻辑
    return super.offer(o);
}

即判定当前线程池中线程数如果小于最大线程数,即直接返回false,达到放入队列失败的效果。
总结
按照以上的调整,只需要通过继承自默认的ThreadPoolExecutor和默认的BlockingQueue(如LinkedBlockingDeque),重载2个主要的方法 ThreadPoolExecutor#execute 和 LinkedBlockingDeque#offer 即达到调整的目的。

5.线程池中的异常状况

当线程池中的一个线程出现异常时,线程池会根据预先设置的策略进行处理。具体处理方式取决于线程池的类型和配置,以下是常见的处理方式:

  1. FixedThreadPool:线程池中的线程出现异常后会立刻终止,同时线程池会创建一个新的线程来替换该线程并继续执行任务。

  2. CachedThreadPool:线程池中的线程出现异常后会被标识为无效线程,线程池会创建一个新的线程来替换该线程并继续执行任务。

  3. ScheduledThreadPool:与FixedThreadPool类似,线程池中的线程出现异常后会立刻终止,同时线程池会创建一个新的线程来替换该线程并继续执行任务。

无论哪种类型的线程池,如果线程出现异常,都应该尽快查找问题所在并进行修复,以保证系统的稳定性和可靠性。可以使用try-catch块来捕获异常并打印日志或进行其他处理。同时,可以使用线程池的execute(Runnable command)方法来提交任务,这样线程池会为每个任务创建一个新的线程,从而减少单个线程出现异常对系统的影响。


总结

本文从线程的创建方式为引入点,详细论述了线程池的基本概念、部分源码及常见的使用方法等。基本囊括了工作中用到的线程池的各种情况及使用方法,可供初级开发者用作线程池的学习资料,中高级开发者的问题排查手册。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值