Java 入门指南:Java 并发编程 —— Java 线程池详解

线程池

线程池(ThreadPool)是一种用于管理和复用线程的机制,它可以预先创建一批线程,并维护一个线程队列,用于执行提交的任务。

线程池的主要目的是提高多线程应用程序的性能和效率,通过重用已创建的线程,减少线程的创建和销毁开销,避免频繁地创建线程和线程上下文切换的性能损耗。

线程池其实是一种池化的技术实现,池化技术的核心思想就是实现资源的复用,避免资源的重复创建和销毁带来的性能开销。

线程池可以管理一系列线程,让线程执行完任务之后不进行销毁,而是继续去处理其它线程已经提交的任务。

应用场景

在 Java 程序中,其实经常需要用到多线程来处理一些业务,但是不建议单纯继承 Thread 类或者实现 Runnable 接口来创建线程,这样会导致频繁创建及销毁线程,同时创建过多的线程也可能引发资源耗尽的风险。

所以使用线程池是一种更合理的选择,方便管理任务,同时实现线程的重复利用。所以线程池一般适合需要异步或者多线程处理任务的场景。例如 Web服务器、并行计算、异步任务处理等场景。

线程池的优点

  1. 提高系统性能:线程池能够更好地利用系统资源,通过复用线程和减少线程的创建和销毁开销,有效提高系统的处理能力。

  2. 提高响应速度:通过线程池,可以将任务在多个线程上并发执行,当任务到达时,任务可以不需要等到线程创建就能立即执行,提高任务的响应速度。

  3. 提供线程管理和监控:线程池可以提供可配置的线程数量、线程保活时间、线程拒绝策略等功能,方便进行线程管理和监控。

  4. 避免资源耗尽:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。通过限制线程数量,可以避免无限制地创建线程导致系统资源耗尽的风险。

使用线程池时,需要注意合理配置线程池的大小和参数,以及根据任务类型和性能需求选择合适的线程池类型。同时,需要注意处理任务执行过程中可能出现的异常和错误,以保证程序的稳定性。

线程池的基本概念

  1. 核心线程数(core pool size):线程池中保持的线程数量,即使这些线程处于空闲状态,也至少会保留这么多线程。
  2. 最大线程数(maximum pool size):线程池允许的最大线程数量,超过这个数量的请求将会排队或被拒绝。
  3. 工作队列(work queue):用来存放等待执行的任务的队列。当线程池中的线程数量达到最大值时,新提交的任务将会被放入这个队列中等待执行。
  4. 拒绝策略(rejection policy):当线程池无法处理更多任务时(即所有线程都在执行任务,并且工作队列已满),线程池会采用某种策略来处理这些任务。常见的拒绝策略包括丢弃任务、使用调用者所在的线程来执行任务等。
  5. 线程工厂(thread factory):用于创建新线程的工厂类,可以根据需要定制新线程的属性。

线程池的构造

Java 提供了一个内置的线程池实现,即 java.util.concurrent.Executors 类。通过 Executors 类可以创建不同类型的线程池,例如固定大小线程池、缓存线程池、定时线程池等。

在这里插入图片描述

  • corePoolSize:线程池中用来工作的核心线程数量。

  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。

  • keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。

  • unit:keepAliveTime 的时间单位。

  • workQueue:任务队列,是一个阻塞队列,当线程数达到核心线程数后,会将任务存储在阻塞队列中。

  • threadFactory :线程池内部创建线程所用的工厂。

  • handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理任务。

创建线程池示例

下面是一个使用 ThreadPoolExecutor 创建线程池的示例:

import java.util.concurrent.*;

public class ThreadPoolExample {

    public static void main(String[] args) {
        // 创建一个线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            2, // 核心线程数
            5, // 最大线程数
            60, // 非核心线程的空闲超时时间(秒)
            TimeUnit.SECONDS, // 时间单位
            new ArrayBlockingQueue<>(10), // 工作队列
            Executors.defaultThreadFactory(), // 线程工厂
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );

        // 提交任务给线程池
        for (int i = 0; i < 10; i++) {
            int taskId = i;
            executor.execute(() -> {
                System.out.println("正在执行任务 " + taskId + ",线程名称:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // 模拟任务执行时间
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

示例说明:

  1. 创建线程池:创建一个 ThreadPoolExecutor 实例,设置核心线程数为 2,最大线程数为 5,非核心线程的空闲超时时间为 60 秒,工作队列为 ArrayBlockingQueue,容量为 10,线程工厂使用默认的工厂,拒绝策略为 CallerRunsPolicy

  2. 提交任务给线程池:提交 10 个任务给线程池,每个任务在执行时输出任务 ID 和当前线程的名称,并模拟任务执行时间为 1 秒。

  3. 关闭线程池,等待所有任务完成。如果在 60 秒内未完成,则强制关闭线程池。如果在等待过程中发生中断,则也强制关闭线程池并设置当前线程的中断状态。

ThreadFactory

ThreadFactory 接口是用来创建新的线程的工厂类。它提供了一个创建线程的方法 newThread(Runnable r),根据传入的 Runnable 实例创建一个新的线程。

以下是 ThreadFactory 接口的代码示例:

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

在使用线程池时,可以自定义实现 ThreadFactory 接口来创建线程,并通过 ThreadPoolExecutor 的构造方法将自定义的 ThreadFactory 对象传递给线程池。这样可以自定义线程的名称、优先级、是否为守护线程等属性

Executor 是Java中异步执行任务的框架,它提供了一种将任务提交给执行者(线程池)并进行执行的方式,以方便地控制多线程任务的执行。

Executor 框架的主要接口是 Executor 接口,它定义了一个用于执行任务的方法:

void execute(Runnable command);

RejectedExecutionHandler

JDK 自带的 RejectedExecutionHandler 实现有 4 种

  • AbortPolicy:丢弃任务,抛出 RunTimeException

  • CallerRunsPolicy:由提交任务的线程来执行任务

  • DiscardPolicy:丢弃这个任务,但是不抛异常

  • DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务

线程池创建的时候,如果不指定拒绝策略就默认是 AbortPolicy 策略。

RejectedExecutionHandler 接口也可以自定义,如将任务存在数据库或者缓存中,这样可以从数据库或者缓存中获取被拒绝掉的任务。

底层机制

执行流程
  1. 刚创建出来的线程池中只有一个构造时传入的阻塞队列,里面并没有线程,如果想要在执行之前创建好核心线程数,可以调用 prestartAllCoreThreads 方法来实现,默认是没有线程的。

  2. 当有线程通过 execute 方法提交了一个任务时,首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数 corePoolSize。

    • 如果小于,那么就直接通过 ThreadFactory 创建一个线程(而不是复用已有的线程)来执行这个任务。当任务执行完之后,线程不会退出,而是会去阻塞队列中获取任务。

    • 如果不小于,尝试将任务放入阻塞队列中。

  3. 尝试将任务放入阻塞队列后,判断任务是否入队成功。

    • 若成功,线程池调用阻塞的线程执行任务

    • 若失败,判断当前线程池里的线程数是否小于最大线程数,也就是入参时的 maximumPoolSize 参数

  4. 若当前线程池里的线程数小于最大线程数,线程池会创建非核心线程来执行提交的任务,注意是优先处理这个提交的任务,而不是从队列中获取已有的任务执行。先提交的任务不一定先执行

  5. 若当前线程池里的线程数不小于最大线程数,此时就会执行拒绝策略,即构造线程池的时候,传入的 RejectedExecutionHandler 对象,来处理这个任务。

![[ThreadPool Working Process.png]]

线程池实现复用的原理

线程在线程池内部被封装成了一个 Worker 对象,Worker 类 继承了 AQS(AbstractQueuedSynchronizer 的缩写,即 抽象队列同步器,是 Java.util.concurrent 中的一个基础工具类),具有一定锁的特性。

![[ThreadPool Worker class.png]]

在创建 Worker 对象的时候,会把线程和任务一起封装到 Worker 内部,然后调用 runWorker 方法来让线程执行任务

runWorker 方法源码:
![[runWorker Source Code.png]]

runWorker() 内部使用了 while 死循环,当第一个任务执行完之后,会不断地通过 getTask 方法获取任务,只要能获取到任务,就会调用 run 方法继续执行任务,这就是线程能够复用的主要原因。

但如果从 getTask 获取不到方法的话,就会调用 finally 中的 processWorkerExit 方法,将线程退出。

在执行任务之前都会调用 Worker 的 lock 方法,执行完任务之后,会调用 unlock 释放锁,这样就可以通过调用 Woker 的 tryLock 方法,根据其加锁状态判断出当前线程是否正在执行任务

在调用 shutdown 方法关闭线程池的时候,就时用这种方式来判断线程有没有在执行任务,如果没有的话,会尝试打断没有执行任务的线程。

线程池的状态

线程池状态存储在 ctl 成员变量中的,ctl 中不仅存储了线程池的状态还存储了当前线程池中线程数的大小

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

线程池内部有 5 个常量来代表线程池的五种状态,在线程池运行过程中,绝大多数操作执行前都需要判断当前线程池处于哪种状态,再来决定是否继续执行该操作。

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;
  • RUNNING:线程池创建时就是这个状态,能够接收新任务,以及对已添加的任务进行处理。

  • SHUTDOWN:调用 shutdown 方法,线程池就会转换成 SHUTDOWN 状态,此时线程池不再接收新任务,但能继续处理已添加的任务到队列中。

  • STOP:调用 shutdownNow 方法,线程池就会转换成 STOP 状态,不接收新任务,也不能继续处理已添加的任务到队列中任务,并且会尝试中断正在处理的任务的线程。

  • TIDYINGSHUTDOWN 状态下,任务数为 0, 其他所有任务已终止,线程池会变为 TIDYING 状态;

    线程池在 SHUTDOWN 状态,任务队列为空且执行中任务为空,线程池会变为 TIDYING 状态;

    线程池在 STOP 状态,线程池中执行中任务为空时,线程池会变为 TIDYING 状态。

  • TERMINATED:线程池彻底终止。线程池在 TIDYING 状态执行完 terminated() 方法就会转变为 TERMINATED 状态。

![[ThreadPool States.png]]

获取任务

线程在执行完任务之后,会继续调用 getTask() 方法获取任务,获取不到任务,线程会退出。

getTask 方法源码:
![[getTask source code.png]]

超时退出机制

getTask() 方法中,下面的代码用来判断当前获取任务的线程是否可以超时退出。

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

如果 allowCoreThreadTimeOut 设置为 true 或者线程池当前的线程数大于核心线程数,那么该获取任务的线程就可以超时退出。

根据是否允许超时来选择调用阻塞队列 workQueuepoll 方法或者 take 方法。

Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
  • 如果允许超时,则调用 poll 方法,传入 keepAliveTime(构造线程池时传入的空闲时间),从队列中阻塞 keepAliveTime 时间来获取任务,获取不到就会返回 null,而一旦 getTask 方法返回 null,线程就会退出。

  • 如果不允许超时,就会调用 take 方法,这个方法会一直阻塞获取任务,直到从队列中获取到任务为止。

![[getTask Working Process.png]]

关闭线程池

关闭一个线程池是一个重要的操作,它可以确保线程池中的线程正常终止并释放相关资源。

在使用线程池的过程中,及时关闭线程池可以避免资源泄漏和意外的线程执行。因此,建议在不再需要使用线程池时及时关闭它。

shutdown

shutdown() 方法:平缓地关闭线程池,使线程池进入 SHUTDOWN 状态。它会停止接受新的任务,尝试中断所有闲置的工作线程,并尝试将已经提交但尚未执行的任务执行完毕。已经在执行的任务会继续执行。
shutdown 方法源码:
![[shutDown source code.png]]

ExecutorService executor = Executors.newFixedThreadPool(10);
// 执行一些任务...
executor.shutdown();
shutdownNow

shutdownNow() 方法:尝试立即关闭线程池,使线程池进入 STOP 状态。它会停止接受新的任务,尝试打断所有的线程,并且尝试中断正在执行的任务。已经提交但尚未执行的任务会被移出阻塞队列。

这个方法返回一个 List,其中包含那些尚未执行的任务。

shutdownNow 方法源码:

![[shutdownNow resource code.png]]

ExecutorService executor = Executors.newFixedThreadPool(10);
// 执行一些任务...
List<Runnable> unexecutedTasks = executor.shutdownNow();
awaitTermination

awaitTermination(long timeout, TimeUnit unit) 方法:等待一段时间来判断线程池是否已经完全关闭。它会阻塞调用线程,直到超过指定的等待时间或者线程池完全关闭。

ExecutorService executor = Executors.newFixedThreadPool(10);
// 执行一些任务...
executor.shutdown();
try {
    if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
        // 如果在指定的等待时间内线程池没有完全关闭,则进行其他操作
    }
} catch (InterruptedException e) {
    // 处理中断异常
    Thread.currentThread().interrupt();
}

监控线程池

在使用线程池的过程中,监控线程池状态是非常重要的,它可以帮助我们更好地了解线程池的运行情况和性能瓶颈,并及时发现、定位和解决问题。

监控本身也会带来一定的性能开销,因此需要在监控和性能之间进行取舍,不要过度监控从而影响性能。同时,及时对发现的问题进行解决,以保证线程池的正常运行。

Java Management Extensions

JMX:Java Management Extensions(简称 JMX)是一个为管理应用程序提供的标准框架。线程池提供了一些 MBean(即可管理的 Java 对象),可以通过 JMX Thead Pool MBean 来监测、管理线程池。可以使用 JConsole 或者其他 JMX 客户端来连接线程池,查看固定间隔时间内线程池的执行情况等等。

原生方法

ThreadPoolExecutor 自身提供了一些用于监控线程池的方法:

  • getActiveCount():获取正在执行任务的线程数。

  • getCompletedTaskCount():获取已经执行完成的任务数

  • getLargestPoolSize():获取线程池曾经创建过的最大的线程数量。这个主要是用来判断线程池是否满过。

  • getPoolSize():获取当前线程池中线程数量的大小。

同时,线程池也预留了很多扩展方法。

比如在 runWorker 方法里面,执行任务之前会回调 beforeExecute 方法,执行任务之后会回调 afterExecute 方法,而这些方法默认都是空实现,可以通过继承 ThreadPoolExecutor 来重写这些方法,实现业务需要的功能。

其他监控工具

除了前面提到的 JMX 外,还可以采用其他的监控工具,如第三方的监控工具 apm 等。这些工具通常会提供更加丰富的监控指标和更加可视化的监控面板。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值