线程池学习笔记


一.使用线程池的好处

  • 降低资源消耗
    通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度
    当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性
    线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控

二.线程池使用场景

1.用和不用有什么区别

  • 不使用线程池的情况下,任务顺序执行
  • 使用线程池的情况下,任务同时执行

2.什么时候适合用

  • 对于多个耗时且互不依赖的任务,可选择使用线程池

    eg. 我们可以做家务的时候可以同时拖地和洗衣服(洗衣机)

  • 对于多个耗时且依赖结果的任务,可选择使用线程池+CountDownLatch
    CountDownLatch是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程执行完后再执行(阻塞)

    eg. 比如做一个项目需求,后端和前端同时开发(却不一定同时结束),这时候快的一方必须等到慢的一方完成还可上线

三.如何使用线程池

一般是通过ThreadPoolExecutor的构造函数来创建线程池,然后提交任务给线程池执行即可。
ThreadPoolExecutor构造函数

1.构造函数参数作用

参数 作用
int corePoolSize 线程池核心线程数
int maximumPoolSize 线程池最大线程数
long keepAliveTime 线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit 时间单位
ThreadFactory threadFactory 线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
BlockingQueue workQueue 阻塞队列,可指定大小

2.参数之间的关系

提交任务时:

  • 如果poolSize<corePoolSize,会新创建一条线程并执行该任务
  • 如果poolSize=corePoolSize,该任务会被放到阻塞队列(workQueue)等待
  • 如果workQueue满了,且poolSize<maxmumPoolSize,会新创建一条线程来执行该任务
  • 如果workQueue满了,且poolSize=maxmumPoolSize,会根据拒绝策略handler拒绝该任务

3.示例

new ThreadPoolExecutor().execute(()->{do sth.});
点击跳转

4.execute、submit区别

  • 传参
    • execute只能接受Runnable类型的任务
    • submit能接受Runnable和Callable类型的任务
  • 返回值
    • execute没有返回值
    • submit有返回值(通过Future.get()获取)
  • 异常
    • execute跟普通线程处理方式一致,通过try-catch捕获异常
    • submit会在call()抛出异常,所以需要Future.get()才能抛出异常

5.建议不同类别的业务用不同的线程池

5.1 为什么呢?

一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务

5.2 错误案例

《线程池运用不当的一次线上事故》

简单来说,就是线程池被父任务占满了,导致所有的子任务没有线程去执行(导致队列堆积),而且父任务会阻塞住,造成死锁
共用线程池死锁图示

四.正确配置线程池参数

  • 如果我们设置的线程池数量太小,如果同一时间有大量任务需要处理,那么就可能会导致大量任务要在任务队列等待,导致OOM。这样的CPU并没有得到充分利用。
  • 如果我们设置的线程池数量太大,大量线程可能会同时争取CPU资源,导致有大量的上下文切换,从而增加线程的执行时间,影响整体的执行效率

所以我们在设置线程池大小时有个公式(N为CPU核心数)

  • CPU密集型任务(N+1) 这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
  • I/O密集型任务(2N+1) 这种任务引用起来,系统会用大部分的事件来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N+1。
    executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() * 2 + 1); 
    executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2 + 1); 
    executor.setQueueCapacity(1000); 
    executor.setKeepAliveSeconds(20); 
    executor.setWaitForTasksToCompleteOnShutdown(true); 
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

五.总结

  • 建议不同场景使用不同的线程池,防止死锁
  • 空间换时间
  • 不允许使用Executors创建线程池,应该通过ThreadPoolExecutor去创建
  • 不同线程池命不同的名(如pool-1-thread-n),有利于定位问题

六.拒绝策略

1.什么时候会触发拒绝策略?

当线程池的任务缓存队列workQueue已满且线程大小poolSize已达到maximumPoolSize,如果这时候还有新任务到来就会采取任务拒绝策略,通常有以下四种策略

  • AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常
  • DiscardPolicy:丢弃任务(不抛出异常)
  • DiscardOldestPolicy:丢弃阻塞队列workQueue最前面的任务,然后执行新任务
  • CallerRunPolicy:由调用线程执行该任务

2.AbortPolicy

创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。

public class AbortPolicyDemo {
  private static final int THREAD_SIZE = 1;
  private static final int CAPACITY = 1;
  public static void main(String[] args) {
    // 创建线程池:指定corePoolSize和maximumPoolSize都为1,阻塞队列大小为1
    ThreadPoolExecutor pool = new ThreadPoolExecutor(THREAD_SIZE, THREAD_SIZE, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(CAPACITY));
    pool.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

    for(int i=1;i<=10;i++) {
      pool.execute(new MyRunnable("task-" + i));
    }
  }

  public static class MyRunnable implements Runnable {
    private String name;
    public MyRunnable(String name) {
      this.name = name;
    }

    @Override
    public void run() {
      System.out.println(this.name + " is running");
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        //ignore
      }
    }
  }
}

测试结果:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.garylee.config.server.controller.admin.AdminLeeObjectConfigController$MyRunnable@3f8f9dd6 rejected from java.util.concurrent.ThreadPoolExecutor@aec6354[Running, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
	at com.garylee.config.server.controller.admin.AdminLeeObjectConfigController.main(AdminLeeObjectConfigController.java:150)
task-1 is running
task-2 is running

结果分析:

  • task-1任务来了,起了一个线程,此时poolSize=corePoolSize=maxmumPoolSize
  • task-2任务来了,发现poolSize=corePoolSize,就把task-2塞入阻塞队列workQueue
  • task-3任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发AbortPolicy拒绝策略,直接抛出RejectExecutionException异常
  • task-1执行完毕
  • task-2执行完毕
  • end

3.DiscardPolicy

创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。

/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

测试结果:

task-1 is running
task-2 is running

结果分析:

  • task-1任务来了,起了一个线程,此时poolSize=corePoolSize=maxmumPoolSize
  • task-2任务来了,发现poolSize=corePoolSize,就把task-2塞入阻塞队列workQueue
  • task-3任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardPolicy拒绝策略,直接丢弃任务
  • task-4任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardPolicy拒绝策略,直接丢弃任务
  • task-10任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardPolicy拒绝策略,直接丢弃任务
  • task-1执行完毕
  • task-2执行完毕
  • end

4.DiscardOldestPolicy

创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。

/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

测试结果:

task-1 is running
task-10 is running

结果分析:

  • task-1任务来了,起了一个线程,此时poolSize=corePoolSize=maxmumPoolSize
  • task-2任务来了,发现poolSize=corePoolSize,就把task-2塞入阻塞队列workQueue
  • task-3任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardOldestPolicy拒绝策略,丢弃阻塞队列workQueue最前面的任务task-2
  • task-4任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardOldestPolicy拒绝策略,丢弃阻塞队列workQueue最前面的任务task-3
  • task-10任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发DiscardOldestPolicy拒绝策略,丢弃阻塞队列workQueue最前面的任务task-9
  • task-1执行完毕
  • task-10执行完毕
  • end

5.CallerRunsPolicy

创建一个corePoolSize和maximumPoolSize都为1,阻塞队列大小为1的线程池。for循环起10个任务,每个任务sleep一秒。

/** 其他代码省略,具体见6.2 /**
pool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

测试结果:

task-1 is running
task-3 is running

task-2 is running
task-5 is running

task-4 is running
task-7 is running

task-6 is running
task-9 is running

task-8 is running
task-10 is running

结果分析:

  • task-1任务来了,起了一个线程,此时poolSize=corePoolSize=maxmumPoolSize
  • task-2任务来了,发现poolSize=corePoolSize,就把task-2塞入阻塞队列workQueue
  • task-3任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发CallerRunsPolicy拒绝策略,加入task-1所在线程执行
  • task-1task-3执行完毕,task-2占用线程
  • task-4任务来了,发现poolSize=corePoolSize,就把task-4塞入阻塞队列workQueue
  • task-5任务来了,发现poolSize=corePoolSize,且阻塞队列workQueue满了,触发CallerRunsPolicy拒绝策略,加入task-2所在线程执行
  • task-2task-5执行完毕,task-4占用线程
  • end

七、线程、线程池状态

1、线程状态

线程有五种状态,分别为:

  • 新建状态(New):表示新创建了一个线程对象
  • 就绪状态(Runnable):线程对象创建后,如果其他线程调用该对象的start方法,该线程就会放在可运行线程池中,变得可运行,等待获取CPU的使用权
  • 运行状态(Running)Running的线程获取了CPU,执行程序代码
  • 阻塞状态(Blocked):线程由于某些原因放弃了CPU使用权,暂时停止运行,直到线程进入Runnable,才有机会转到Running
  • 死亡状态(Dead):线程由于执行完毕或异常退出run方法,结束了生命周期

2、线程池状态

  • 运行状态(RUNNING):表示线程池正常运行,能接受新任务,也能正常处理队列中的任务
  • 关闭状态(SHUTDOWN):表示线程池处于关闭状态,不能接受新任务,能正常处理队列中任务
    • 状态切换:调用shutdown()方法,线程池状态由RUNNING -> SHUTDOWN
  • 停止状态(STOP):表示线程池处于停止状态,不能接受新任务,不能正常处理队列中任务,正在运行的线程也会停止
    • 状态切换:调用shutdownnow()方法,线程池状态由(RUNNING or SHUTDOWN) -> STOP
  • 整理状态(TIDYING):线程池没线程在运行时,状态会自动变为TIDYING,并且会调用terminated()方法(该方法是空实现,留给调用方扩展)
    • 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING
  • 销毁状态(TERMINATED):表示线程池已彻底终止
    • 状态切换:调用terminated()方法,线程池状态由TIDYING -> TERMINATED

八、一些问题

  • 核心线程数可以设置为0吗?
  • 非核心线程什么时候会销毁?销毁的判断依据是什么?:空闲的时候
  • shutdown和shutdownnow的区别?

文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录