[JUC] 线程池浅析

线程池

1. 什么是线程池?

​ 线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程的创建和任务的执行解耦开来。可以通过创建线程池来复用已创建的线程来降低频繁创建和销毁线程所带来的资源消耗。

1.1 线程池的优点

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

1.2 Java中的线程池框架

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors, ExecutorService,ThreadPoolExecutor 这几个类\

image-20231205192933808

​ 其中,Executor 接口只是简单的定义了一个提交任务的 execute() 方法,但它却表达了线程池的一个最基本的思想:将任务的执行和提交解耦开来。Executor 基于生产者-消费者模式,提交任务的操作相当于生产者(生成待完成的工作单元),工作线程从任务队列中获取任务并执行则相当于消费者。

​ 由于Executor 接口过于简单,一个线程池应该还要包含返回任务的 Future、提交 Callable 任务、关闭线程池、批量提交任务等。所以在在 Executor 上扩展了 ExecutorService 接口,它继承自 Executor,完善了线程池一些基础操作的定义。

ThreadPoolExecutor 是 Java 提供的通用的基础的线程池实现。Java 中已经提供了一些常用的执行策略的线程池,它们位于 java.util.concurrent.Executors 中,这些线程池都是通过配置不同的策略的 ThreadPoolExecutor 实现的。

2. ThreadPoolExecutor类的构造参数说明

​ ThreadPoolExecutor 是 Java 提供的一个基础的通用的线程线程池实现。可以直接创建一个ThreadPoolExecutor 对象来创建一个线程池,它提供了丰富的配置参数,通过指定不同参数值可以灵活的定制出符合我们需求的线程池出来。它的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler);

2.1 int corePoolSize :线程池的核心线程数

首先明确俩个概念:

核心线程:线程池新建线程的时候,当前活动的线程总数< corePoolSize,新建的线程即为核心线程。

非核心线程:线程池新建线程的时候,当前活动的线程总数> corePoolSize且阻塞队列已满,这时新建一个线程来执行新提交的任务即为非核心线程。

​ 核心线程默认情况下会一直存活在线程池中,即使这个核心线程不工作(空闲状态),除非ThreadPoolExecutor 的 allowCoreThreadTimeOut这个属性为 true,那么核心线程如果空闲状态下,超过一定时间后就被销毁。

2.2 int maximumPoolSize :能容纳的最大线程数

​ 线程池中允许的最大线程数,最大线程数 = 核心线程数 + 非核心线程数

2.3 long keepAliveTime:空闲线程存活时间

​ 即为空闲线程允许的最大的存活时间。如果一个非核心线程空闲状态的时长超过keepAliveTime了,就会被销毁掉。注意:如果设置allowCoreThreadTimeOut = true,就变成核心线程超时销毁了。

2.4 TimeUnit unit:存活的时间单位

​ TimeUnit 是一个枚举类型,列举如下:

单位说明
NANOSECONDS1纳秒 = 1微秒 / 1000
MICROSECONDS1微秒 = 1毫秒 / 1000
MILLISECONDS1毫秒 = 1秒 /1000
SECONDS
MINUTES
HOURS小时
DAYS

2.5 BlockingQueue workQueue:存放提交但未执行任务的队列

​ 当核心线程都在工作的时候,新提交的任务就会被添加到这个工作阻塞队列中进行排队等待;如果阻塞队列也满了,线程池就新建非核心线程去执行任务。workQueue维护的是等待执行的Runnable对象。 常用的类型:(无界队列、有界队列、同步移交队列)

2.6 ThreadFactory threadFactory:创建线程的工厂类

​ 线程工厂 ThreadFactory 接口只定义了一个方法:

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

​ 在 ThreadPoolExecutor 构造方法中的 threadFactory 参数可以指定线程工厂,每当线程池需要创建一个新线程时都会调用线程工厂的 newThread 方法。通过指定一个线程工厂,可以定制线程池中的线程。

2.7 RejectedExecutionHandler handler:等待队列满后的拒绝策略(饱和策略)

​ 是指当线程池中的工作队列已满,且线程池中的线程已达到最大值时,线程池无法再处理新的任务时所采取的策略。

​ 当使用有界队列并且队列被填满后,或向一个已经关闭的 Executor 提交任务时,饱和策略就会发挥作用。饱和策略通过 ThreadPoolExecutor 的 setRejectedExecutionHandler() 方法进行设置。

​ 饱和策略 RejectedExecutionHandler 是一个接口,只定义了一个 rejectedExecution 方法用于接收被拒绝的任务。

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

Java 线程池中提供了以下四种拒绝策略:

(1)AbortPolicy:中止(Abort)策略,是 ThreadPoolExecutor 默认的饱和策略,直接抛出 RejectedExecutionException 异常,该异常可以在代码中捕获并进行处理。
(2)CallerRunsPolicy:“调用者执行”策略,该策略既不会抛出异常也不会抛弃任务,而是在提交任务的线程中执行任务,即在调用 execute 的线程中执行。在队列满了之后使用提交任务的线程来执行任务可以降低任务提交的速率。
(3)DiscardOldestPolicy:“抛弃最旧的”策略,当有任务将被拒绝时将抛弃下一个将被执行的任务,然后尝试重新提交新的任务。如果是一个优先队列,那么该策略将导致优先级最高任务被抛弃,因此最好不要将该饱和策略和优先级队列放在一起使用。
(4)DiscardPolicy:丢弃策略,被拒绝的任务会被直接丢弃,不作任何处理。

CallerRunsPolicy 拒绝策略:

	CallerRunsPolicy 拒绝策略,当线程池的工作队列已满且线程池中的线程数已达到最大值时,线程池无法再处理新的任务时,它会将该任务交给线程池的调用线程来执行,即使用提交任务的线程来执行该任务。

​ CallerRunsPolicy 拒绝策略主要用于防止任务丢失和保证任务的顺序性。当线程池无法处理新的任务时,CallerRunsPolicy 会将任务交给调用线程来执行,这样可以确保任务不会丢失,并且可以保证任务的顺序性,即任务的执行顺序和提交顺序一致。

但是,使用 CallerRunsPolicy 拒绝策略可能会导致调用线程被阻塞,因为调用线程需要等待任务执行完毕才能继续执行其他任务,这可能会影响整个系统的性能。因此,在选择拒绝策略时需要权衡任务执行的顺序性和系统的性能。

3. 线程池的工作流程

image-20231205202458102

当一个任务被添加进线程池时:

  1. 当前线程数量未达到 corePoolSize,则新建一个线程(核心线程)执行任务
  2. 当前线程数量达到了 corePoolSize,则将任务移入阻塞队列等待,之后让空闲的线程来处理;
  3. 当阻塞队列已满,且核心线程都未空闲,则新建线程(非核心线程)来执行任务
  4. 当阻塞队列已满,总线程数又达到了 maximumPoolSize最大线程数,就会按照拒绝策略处理无法执行的任务,比如AbortPolicy策略会直接抛出RejectedExecutionHandler。

4. 常用线程池的种类与创建

​ 以下都通过Executors类进行静态方法的创建

4.1 FixedThreadPool

​ 可重用固定线程数的线程池,超出的线程会在队列中等待,在Executors类中我们可以找到创建方式:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

FixedThreadPoolcorePoolSizemaximumPoolSize都设置为参数nThreads,也就是只有固定数量的核心线程,不存在非核心线程。keepAliveTime为0L表示多余的线程立刻终止,因为不会产生多余的线程,所以这个参数是无效的。FixedThreadPool的任务队列采用的是LinkedBlockingQueue。

img

创建线程池的方法,在我们的程序中只需要如下使用,后面其他种类的同理:

public static void main(String[] args) {
        // 参数是要线程池的线程最大值
        ExecutorService executorService = Executors.newFixedThreadPool(10);
}

4.2 CachedThreadPool

CachedThreadPool是一个根据需要创建线程的线程池

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

CachedThreadPoolcorePoolSize是0,maximumPoolSize是Int的最大值,也就是说CachedThreadPool没有核心线程,全部都是非核心线程,并且没有上限。keepAliveTime是60秒,就是说空闲线程等待新任务60秒,超时则销毁。此处用到的队列是阻塞队列SynchronousQueue,这个队列没有缓冲区,所以其中最多只能存在一个元素,有新的任务则阻塞等待。

img

4.3 SingleThreadExecutor

SingleThreadExecutor是使用单个线程工作的线程池。其创建源码如下:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

可以看到总线程数和核心线程数都是1,所以就只有一个核心线程。该线程池才用链表阻塞队列LinkedBlockingQueue,先进先出原则,所以保证了任务的按顺序逐一进行。

img

4.4 ScheduledThreadPool

ScheduledThreadPool是一个能实现定时和周期性任务的线程池,它的创建源码如下:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

这里创建了ScheduledThreadPoolExecutor,继承自ThreadPoolExecutor,主要用于定时延时或者定期处理任务。ScheduledThreadPoolExecutor的构造如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

可以看出corePoolSize是传进来的固定值,maximumPoolSize无限大,因为采用的队列DelayedWorkQueue是无界的,所以maximumPoolSize参数无效。该线程池执行如下:

img

​ 当执行scheduleAtFixedRate或者scheduleWithFixedDelay方法时,会向DelayedWorkQueue添加一个实现RunnableScheduledFuture接口的ScheduledFutureTask(任务的包装类),并会检查运行的线程是否达到corePoolSize。如果没有则新建线程并启动ScheduledFutureTask,然后去执行任务。如果运行的线程达到了corePoolSize时,则将任务添加到DelayedWorkQueue中。DelayedWorkQueue会将任务进行排序,先要执行的任务会放在队列的前面。在跟此前介绍的线程池不同的是,当执行完任务后,会将ScheduledFutureTask中的time变量改为下次要执行的时间并放回到DelayedWorkQueue中。

注意:

​ 一般开发中,尽量使用ThreadPoolExecutor类自定义创建线程池,而不适用Executors的几种线程池。原因如下:

image-20231205205559206

文章引用

链接:https://juejin.cn/post/6844904146856837128
链接:https://juejin.cn/post/6844903560623161352

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值