【线程池】ThreadPoolExecutor结构以及构造器参数详解

线程池的介绍

不用线程池存在的问题

  • 反复创建销毁线程开销大
  • 过多的线程占用大量内存

线程池的使用,就类似于计划经济,控制资源总量,复用线程。有如下3点好处。

  1. 加快响应速度。消除了线程创建带来的延时。
  2. 合理的利用CPU和内存。控制线程的数量,既不会因为线程太多导致内存溢出,也不会因为太少导致CPU资源的浪费。
  3. 便于统一管理线程。例如数据统计。

线程的使用场景举例:

  • 服务器接收大量请求,使用线程池可以大大减少线程创建和销毁次数,提高服务器工作效率。
  • 开发中使用多个线程时,可以考虑采用线程池。

线程池结构

在这里插入图片描述
ThreadPoolExecutor继承AbstractExecutorService(实现ExecutorService),4个类/接口都可以视作线程池,并向上转型(多态)。ThreadPoolExecutor中有5个嵌套类,其中4个为RejectedExecutionHandler拒绝策略接口的实现类,1个是Worker类,用于维护正在运行任务的线程的中断控制状态,以及其他的次要信息。

Executors是线程池工具类,可以通过静态工厂方法快捷实现线程池,例如Executors.newFixedThreadPool(10).

举个面包店的例子
假设有一所面包店,面包店平时有5位面包师,每天随着订单增加,5位师傅逐渐加入到工作中去。不能及时处理的订单,会挂在墙上的订单栏上,等待面包师顺序处理。
订单过多时,5个面包师忙不过来就招临时面包师,最多5名。根据任务多少会调整临时面包师的数量,太长时间没事干的临时面包师会被辞退掉。
店铺关闭或者订单在面包师达到10名后,便不再接新单。

线程池构造器

在这里插入图片描述
ThreadPoolExecutor只有四个构造器,最多有7个参数。其中前5个是必要参数,线程工厂和拒绝策略可选。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    
    
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

在这里插入图片描述

线程添加策略

  1. 如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,仍会创建新线程运行新任务。【疑问:是一下全建出来,还是一个一个创建】
  2. 如果线程数大于corePoolSize但是少于maxPoolSize,则将任务放入到阻塞队列中。
  3. 如果队列已满,并且线程数小于maxPoolSize,则创建一个新线程运行任务。
  4. 如果队列已满,并且线程数大于或等于maxPoolSize,则拒绝该任务。

在这里插入图片描述

增减线程的特点

  1. 如果设置相同的corePoolSize和maxThreadPoolSize,相当于创建固定大小的线程池。此时,工作队列通常选用无界队列,超时时间为0L。
  2. 线程池希望保持较少的线程数,只在负载变得很大的时候才增加新的线程。
  3. 通过设置maxPoolSize为Integer.MAX_VALUE,可以允许线程池容纳任意容量的并发任务。
  4. 只有在队列填满时,才会创建多于corePoolSize的新线程。如果采用无界队列,例如LinkedBlockingQueue,线程数不会超过corePoolSize。

线程池的拒绝策略

拒绝时机–参见execute方法中两个调用位置

final void reject(Runnable command) {
    
    
    handler.rejectedExecution(command, this);
}
  1. 当Executor关闭时,提交新任务会被拒绝;
  2. 当Executor对最大线程数和工作队列容量使用有限边界,并且已经饱和时。

拒绝策略

  1. AbortPolicy:直接抛出异常,提示没有提交成功
  2. DiscardPolicy:直接丢弃新任务,不抛出异常。
  3. DiscardOldestPolicy:丢弃最老的任务
  4. CallerRunsPolicy:让提交任务的线程执行。好处是避免了业务损失;提交速度降低(主线程一直提交任务,线程池和工作队列满后,主线程开始执行提交的任务,相当于给了线程池一个缓冲的时间)。

源码分析

public interface RejectedExecutionHandler {
    
    
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

拒绝策略接口只有一个方法,目的很明确。

4种拒绝策略实现RejectedExecutionHandler接口,代码实现也很简单清楚,此处只介绍DiscardOldestPolicy和CallerRunsPolicy两种策略。

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    
    
    public DiscardOldestPolicy() {
    
     }
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        // 1. 如果线程池是RUNNING状态,并且线程达到饱和,则丢弃最老的任务,重新执行execute方法
        // 2. 如果线程池非RUNNING状态,则直接丢弃任务。
        if (!e.isShutdown()) {
    
    
            e.getQueue().poll();
            e.execute(r);
        }
    }
}
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    
    
    public CallerRunsPolicy() {
    
     }
    // 1. 如果线程池是RUNNING状态,并且线程达到饱和,则使用调用者线程执行任务
    // 2. 如果线程池非RUNNING状态,则直接丢弃任务。
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    
    
        if (!e.isShutdown()) {
    
    
            r.run();
        }
    }
}

我们知道直接调用runnable的run()方法是同步调用,调用者线程也就是调用当前线程池execute方法的那个线程。

工作队列

工作队列通用策略有3种。

  • 直接提交队列:默认为SynchronousQueue,直接将任务提交给线程,不保持他们。如果不存在空闲的线程,则试图将任务加入队列将失败,会新创建一个线程。通常要求无界
    maximumPoolSizes 。
  • 无界队列:例如LinkedBlockingQueue,用于突发请求。
  • 有界队列:例如ArrayBlockingQueue,有利于防止资源耗尽,较难调整队列大小和最大池大小的值。

线程工厂

public interface ThreadFactory {
    
    
    Thread newThread(Runnable r);
}

ThreadFactory只有一个方法,是一个函数式接口。

static class DefaultThreadFactory 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 = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
    
    
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        // 只生产用户线程
        if (t.isDaemon())
            t.setDaemon(false);
        // 线程的安全级别是5
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

常见的线程池

Executors快捷生成的线程池参数总结。
在这里插入图片描述

默认的线程工厂ThreadFactory是Executors.defaultThreadFactory(),拒绝策略是AbortPolicy。

  • SingleThreadPool:内部和FixedThreadPool基本一致,只是线程数不同。
  • CachedThreadPool:创建可缓存线程池。是无界线程池,具有自动回收多余线程的功能。采用的是同步移交队列,任务直接给到空闲线程或者创建新线程执行。如果线程空闲(没有任务执行)60秒,回收线程。
  • ScheduledThreadPool:支持定时及周期性任务执行的线程池。
  • WorkStealingPool是JDK8中新增的一种线程池,是新的线程池类ForkJoinPool的扩展,能够合理地使用CPU对任务做并行操作,适合耗时的场景,例如递归、分而治之,并且不加锁的场景。

线程池的注意点

避免任务堆积,避免线程数过度增加,排查线程泄漏。线程多时,有可能存在线程泄漏,线程执行完毕但是没被回收,可能是任务逻辑问题。

猜你喜欢

转载自blog.csdn.net/LIZHONGPING00/article/details/105155805
今日推荐