线程池的概念
线程池是一种基于池化技能的多线程运用形式,它预先创建了肯定数量的线程,并将这些线程放入一个容器中(即线程池)举行管理。当需要执行新的任务时,不是直接创建新的线程,而是从线程池中取出一个空闲的线程来执行这个任务。
线程池的优缺点
长处:
- 资源复用:线程池中的线程可以被重复利用,制止了因频仍创建和烧毁线程所带来的性能开销。这对于需要大量线程的应用程序来说,可以明显提高程序的执行效率。
- 提高体系响应速度:当任务到达时,可以直接从线程池中取出空闲的线程来执行,而不需要等待新线程的创建和初始化,从而加快了任务的执行速度,提高了体系的响应性。
- 线程管理:线程池提供了对线程的统一管理,包括线程的创建、烧毁、调治等。这有助于淘汰因线程管理不当而导致的资源泄漏和死锁等题目。
- 可控制并发数:通过线程池,我们可以很方便地控制体系中并发线程的数量,从而制止因为并发线程过多而导致的体系资源耗尽或体系瓦解等题目。
- 支持并发任务的灵活调治:线程池提供了灵活的调治策略,可以根据任务的重要性和紧急程度来公道地分配线程资源,确保重要和紧急的任务能够优先得到执行。
缺点:
- 线程池巨细限定:
- 线程池中的线程数量是有限定的,这大概会导致在极端高并发环境下,线程池中的线程全部被占用,新提交的任务需要等待空闲线程,从而增加了任务的等待时间。
- 如果线程池的最大线程数设置不当,过小会导致任务处理惩罚不外来,过大则大概导致体系资源(如CPU、内存)过度斲丧,影响体系性能。
- 任务队列限定:
- 线程池通常会将无法立即执行的任务放入到任务队列中等待。但是,如果任务队列的容量也有限定,当队列满时,新提交的任务大概会被拒绝,这大概会导致部门任务丢失或需要额外的处理惩罚逻辑。
- 比方,在某些线程池实现中,如果队列已满且无法创建新线程(因为已达到最大线程数),则大概会执行拒绝策略,如抛出异常、丢弃任务等。
- 线程上下文切换开销:
- 虽然线程池通过复用线程淘汰了线程创建和烧毁的开销,但在高并发场景下,线程之间的上下文切换仍然是一个不可忽视的开销。频仍的上下文切换会导致CPU时间被浪费在生存和恢复线程状态上,从而低落体系的整体性能。
- 复杂度和可维护性:
- 利用线程池需要公道配置线程池的参数(如核心线程数、最大线程数、任务队列容量等),这增加了程序的复杂度和配置难度。
- 线程池的错误处理惩罚和异常管理也相对复杂,需要程序员具备较高的并发编程本领和异常处理惩罚本领。
- 不适用于全部场景:
- 线程池适用于那些需要频仍创建和烧毁线程,且任务执行时间相对较短的场景。对于执行时间非常长或数量较少的任务,利用线程池大概并不合适,因为线程池中的线程大概会长时间处于空闲状态,浪费体系资源。
线程池的实现
在Java中,java.util.concurrent包提供了多种线程池的实现,如ThreadPoolExecutor、ScheduledThreadPoolExecutor等,它们都是基于ExecutorService接口的实现。通过这些线程池实现,我们可以很方便地创建和管理线程池,以满足不同的并发需求。
常见的Java线程池实现:
ThreadPoolExecutor
这是Java中最核心、最通用的线程池实现。它提供了丰富的参数配置,如核心线程数、最大线程数、任务队列容量、线程存活时间等,允许用户根据具体需求灵活调解线程池的行为。
ThreadPoolExecutor还支持自定义线程工厂和拒绝策略,以满足更复杂的需求。
利用ThreadPoolExecutor实现线程池
这个示例将展示如何创建一个线程池,提交任务到线程池,并等待全部任务完成。
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
- public class ThreadPoolExecutorExample {
- public static void main(String[] args) {
- // 创建一个固定大小的线程池
- // 参数分别为:核心线程数、最大线程数、空闲线程存活时间、时间单位、任务队列(这里使用无界队列)
- ExecutorService executorService = Executors.newFixedThreadPool(5);
- // 或者直接使用ThreadPoolExecutor构造函数来创建,这样可以更灵活地配置参数
- // ExecutorService executorService = new ThreadPoolExecutor(
- // 5, // 核心线程数
- // 10, // 最大线程数
- // 60L, // 空闲线程存活时间
- // TimeUnit.SECONDS, // 时间单位
- // new java.util.concurrent.ArrayBlockingQueue<>(100) // 任务队列
- // );
- // 提交任务到线程池
- for (int i = 0; i < 10; i++) {
- final int taskId = i;
- executorService.submit(() -> {
- // 模拟任务执行
- System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
- try {
- // 假设任务执行需要一些时间
- TimeUnit.SECONDS.sleep(1);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- });
- }
- // 关闭线程池,不再接受新任务,但已提交的任务会继续执行
- executorService.shutdown();
- // 等待所有任务完成
- try {
- if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
- // 如果在指定时间内没有完成,则尝试停止当前正在执行的任务
- executorService.shutdownNow();
- // 等待正在执行的任务停止
- if (!executorService.awaitTermination(60, TimeUnit.SECONDS))
- System.err.println("Pool did not terminate");
- }
- } catch (InterruptedException ie) {
- // 当前线程在等待过程中被中断
- executorService.shutdownNow();
- // 保存中断状态
- Thread.currentThread().interrupt();
- }
- System.out.println("Finished all tasks");
- }
- }
复制代码 在这个示例中,我们首先创建了一个固定巨细的线程池,然后提交了10个任务到线程池。每个任务都简朴地打印出它的ID和执行它的线程名称,并模拟执行了一段时间(通过TimeUnit.SECONDS.sleep(1);)。末了,我们关闭了线程池,并等待全部任务完成。
留意,在实际应用中,你大概需要根据具体需求调解线程池的配置参数,如核心线程数、最大线程数、空闲线程存活时间等。此外,对于任务队列的选择也需要根据任务的性质来决定,比如是否允许有界队列、队列的巨细等。
利用ThreadPoolExecutor实现线程池的优缺点
长处:
- 资源复用:线程池中的线程可以被重复利用,制止了频仍创建和烧毁线程所带来的开销,这对于需要频仍执行短任务的场景尤为有利。
- 提高体系响应速度:当任务到达时,线程池能够迅速响应并分配线程来执行,淘汰了任务的等待时间,提高了体系的响应性。
- 控制并发数:通过配置线程池的参数,可以精确控制体系中同时运行的线程数量,这有助于制止因过多线程同时运行而导致的资源耗尽或体系瓦解等题目。
- 提供灵活的调治策略:ThreadPoolExecutor提供了丰富的调治策略,如任务队列的选择(阻塞队列、同步队列等)、线程工厂的设置以及拒绝策略的实现等,使得用户可以根据实际需求灵活配置线程池的行为。
- 提高体系稳固性:通过公道配置线程池,可以有效地控制线程的生命周期和并发量,从而低落体系因线程管理不当而导致的瓦解风险。
缺点:
- 线程池巨细限定:线程池中的线程数量是有限定的,如果全部线程都在忙碌,新到达的任务大概需要等待空闲线程,这大概会导致任务延迟执行。
- 任务队列的容量限定:如果任务队列的容量也有限定,并且全部线程都在忙碌,当队列满时,新到达的任务大概会被拒绝执行,除非配置了合适的拒绝策略。
- 线程上下文切换开销:虽然线程池淘汰了线程创建和烧毁的开销,但在高并发场景下,线程之间的上下文切换仍然是一个不可忽视的开销。过多的上下文切换会低落体系的整体性能。
- 配置复杂度:ThreadPoolExecutor提供了丰富的配置选项,但同时也增加了配置的复杂度。不公道的配置大概会导致线程池性能不佳或资源浪费。
- 不适用于全部场景:线程池特别适用于需要频仍创建和烧毁线程的场景,但对于执行时间非常长或数量较少的任务,利用线程池大概并不合适,因为线程池中的线程大概会长时间处于空闲状态,浪费体系资源。
ScheduledThreadPoolExecutor
这是一个继承自ThreadPoolExecutor的线程池实现,专门用于在给定的延迟后运行命令,大概定期地执行命令。
它支持调治任务在未来的某个时间点执行,大概按照指定的频率周期性执行。
ScheduledThreadPoolExecutor实现线程池
这个示例将展示如何创建一个ScheduledThreadPoolExecutor,并提交一个周期性执行的任务。
- import java.util.concurrent.Executors;
- import java.util.concurrent.ScheduledExecutorService;
- import java.util.concurrent.TimeUnit;
- public class ScheduledThreadPoolExecutorExample {
- public static void main(String[] args) {
- // 创建一个ScheduledThreadPoolExecutor,其线程池大小为3
- ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
- // 提交一个周期性执行的任务
- // 这里的任务是在控制台打印当前时间,每2秒执行一次
- Runnable periodicTask = () -> {
- System.out.println("执行任务: " + System.currentTimeMillis());
- };
- // 初始延迟为0,表示立即开始执行;之后每隔2秒执行一次
- scheduledExecutorService.scheduleAtFixedRate(periodicTask, 0, 2, TimeUnit.SECONDS);
- // 注意:在实际应用中,你可能需要某种方式来关闭线程池。
- // 这里为了简化示例,我们没有添加关闭线程池的代码。
- // 在实际应用中,你应该在适当的时候调用shutdown()或shutdownNow()方法来关闭线程池。
- // 注意:这个示例中的main方法会立即返回,但ScheduledThreadPoolExecutor中的任务会继续在后台执行。
- // 如果你希望main方法等待直到所有任务都完成(对于ScheduledThreadPoolExecutor来说,这通常是不现实的,
- // 因为周期性任务可能会永远执行下去),你需要使用其他同步机制。
- // 但在这个简单的示例中,我们不需要这样做。
- }
- }
复制代码 在这个示例中,我们首先创建了一个ScheduledThreadPoolExecutor,其线程池巨细为3。然后,我们定义了一个简朴的任务,该任务只是打印出当前的时间戳。我们利用scheduleAtFixedRate方法提交了这个任务,指定了初始延迟为0(表现立即开始执行),之后每隔2秒执行一次。
请留意,这个示例中的main方法会立即返回,但ScheduledThreadPoolExecutor中的任务会继续在后台执行。在实际应用中,你大概需要在得当的时间关闭线程池,以释放资源。这可以通过调用shutdown()或shutdownNow()方法来实现。然而,在这个简朴的示例中,我们没有添加如许的代码。
利用ScheduledThreadPoolExecutor实现线程池的优缺点
长处:
- 周期性任务支持:ScheduledThreadPoolExecutor特别适用于需要周期性执行的任务。它能够按照指定的时间隔断或延迟时间自动调治任务执行,非常适合于需要定时执行任务的场景。
- 资源复用:与ThreadPoolExecutor类似,ScheduledThreadPoolExecutor也实现了线程的复用,淘汰了线程的创建和烧毁开销,提高了体系的资源利用率。
- 灵活的任务调治:通过schedule、scheduleAtFixedRate和scheduleWithFixedDelay等方法,可以灵活地安排任务的执行时间,包括一次性延迟执行、固定频率执行以及固定延迟执行等多种模式。
- 易于利用:ScheduledThreadPoolExecutor提供了简洁的API接口,使得任务调治变得简朴直观,低落了开发难度。
缺点:
- 任务调治开销:虽然ScheduledThreadPoolExecutor提供了灵活的任务调治功能,但这种调治机制自己也会带来肯定的开销。特别是在高并发场景下,频仍的任务调治大概会增加体系的负担。
- 任务执行顺序:对于利用scheduleAtFixedRate方法提交的任务,如果某个任务的执行时间高出了调治隔断,那么下一个任务将会立即在上一个任务完成后开始执行,而不会等待完整的调治隔断。这大概会导致任务在短时间内一连执行多次,从而影响到任务的执行效果和体系的稳固性。
- 任务取消和中断:虽然ScheduledThreadPoolExecutor提供了取消任务的方法,但取消任务并不总是立即生效的。特别是对于那些已经开始执行的任务,取消操作大概无法立即中断其执行。此外,如果任务在执行过程中没有正确处理惩罚中断信号,那么取消操作大概无法达到预期的效果。
- 资源限定:与ThreadPoolExecutor一样,ScheduledThreadPoolExecutor中的线程数量也是有限的。如果全部线程都在忙碌,新提交的任务大概会被放入任务队列中等待执行。如果任务队列也满了,那么新提交的任务大概会被拒绝执行(除非配置了合适的拒绝策略)。
Executors工厂类:
虽然Executors不是一个直接的线程池实现,但它提供了一系列静态方法来创建不同类型的线程池。
比方,Executors.newFixedThreadPool(int nThreads)用于创建一个可重用固定线程数的线程池;Executors.newSingleThreadExecutor()用于创建一个单线程的Executor,它包管全部任务都在同一个线程中按顺序执行;Executors.newCachedThreadPool()则用于创建一个根据需要创建新线程的线程池,但每个空闲线程将在60秒后自动终止。
利用Executors工厂类实现线程池
这个示例将展示如何创建一个固定巨细的线程池,并提交一些任务到该线程池执行。
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class ExecutorsExample {
- public static void main(String[] args) {
- // 使用Executors工厂类创建一个固定大小的线程池,这里设置线程池大小为5
- ExecutorService executorService = Executors.newFixedThreadPool(5);
- // 提交任务到线程池
- for (int i = 0; i < 10; i++) {
- final int taskId = i;
- executorService.submit(() -> {
- // 这里模拟任务执行,比如打印任务ID和执行它的线程名称
- System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
- // 注意:在实际应用中,你可能需要在这里添加一些耗时的操作,比如数据库访问、文件IO等
- });
- }
- // 关闭线程池,不再接受新任务,但已提交的任务会继续执行
- // 注意:shutdown()方法不会等待线程池中的任务执行完成,它只是不再接受新任务
- // 如果你需要等待所有任务完成,可以使用shutdown()后跟上awaitTermination(),或者直接使用awaitTermination(long timeout, TimeUnit unit)
- // 但为了简化示例,这里只调用shutdown()
- executorService.shutdown();
- // 注意:在实际应用中,你可能需要添加一些逻辑来等待所有任务完成,
- // 但在这个简单的示例中,我们假设主线程(即main方法)不需要等待线程池中的任务完成。
- }
- }
复制代码 在这个示例中,我们首先利用Executors.newFixedThreadPool(5)创建了一个固定巨细为5的线程池。然后,我们通过一个循环提交了10个任务到线程池。每个任务都简朴地打印出它的ID和执行它的线程名称。末了,我们调用了shutdown()方法来关闭线程池,这表现线程池将不再接受新的任务,但已经提交的任务会继续执行直到完成。
请留意,这个示例中的main方法会立即返回,但线程池中的任务大概会在main方法返回之后继续执行。如果你需要等待全部任务完成,可以考虑利用awaitTermination()方法。然而,在这个简朴的示例中,我们没有包含如许的逻辑。
利用Executors工厂类实现线程池的优缺点
长处:
- 轻巧快捷:Executors工厂类提供了一系列静态方法来快速创建不同配置的线程池,这使得开发者无需深入了解ThreadPoolExecutor的全部细节,就能方便地创建符合需求的线程池。
- 灵活配置:虽然Executors工厂类提供的方法相对简朴,但它们已经覆盖了大多数常见的线程池配置需求,如固定巨细的线程池、可缓存的线程池以及单线程的线程池等。
- 代码可读性:利用Executors工厂类创建的线程池代码更加简洁明了,提高了代码的可读性和可维护性。
缺点:
- 隐藏细节:由于Executors工厂类封装了线程池的具体实现细节,这大概导致开发者对线程池的内部机制了解不够深入,从而在某些复杂场景下难以做出正确的决策。
- 默认配置大概不适合全部场景:Executors工厂类提供的默认配置大概并不适合全部场景。比方,newCachedThreadPool方法创建的线程池允许线程数量无限增长,这大概会在某些环境下导致资源耗尽。
- 无法直接调解核心参数:如果开发者需要调解线程池的核心参数(如核心线程数、最大线程数、任务队列容量等),利用Executors工厂类大概会受到限定,因为某些方法并不直接袒露这些参数的设置接口。
ForkJoinPool:
ForkJoinPool是Java 7引入的一种特殊的线程池,专为执行分而治之算法(如归并排序)而筹划。
它利用了一种称为工作窃取(work-stealing)的算法,允许线程从其他线程的队列中窃取任务来执行,从而提高了任务处理惩罚的效率和吞吐量。
利用ForkJoinPool实现线程池
ForkJoinPool是Java 7中引入的一种特殊的线程池,它特别适用于执行分而治之(divide-and-conquer)算法的任务。
示例展示了如何利用ForkJoinPool来并行计算一组数的和:
- import java.util.Arrays;
- import java.util.concurrent.ForkJoinPool;
- import java.util.concurrent.RecursiveTask;
- // 定义一个继承自RecursiveTask的类,用于递归分割任务
- class SumTask extends RecursiveTask<Integer> {
- private int[] numbers;
- private int start;
- private int end;
- // 构造函数,用于初始化任务
- public SumTask(int[] numbers, int start, int end) {
- this.numbers = numbers;
- this.start = start;
- this.end = end;
- }
- // 递归分割任务,当任务足够小时直接计算结果
- @Override
- protected Integer compute() {
- int length = end - start;
- if (length <= 1) {
- return numbers[start];
- }
- // 将任务分割成两半
- int split = start + length / 2;
- SumTask leftTask = new SumTask(numbers, start, split);
- SumTask rightTask = new SumTask(numbers, split, end);
- // 提交子任务到ForkJoinPool
- leftTask.fork();
- int rightResult = rightTask.compute();
- // 等待子任务完成,并合并结果
- return leftTask.join() + rightResult;
- }
- public static void main(String[] args) {
- int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
- ForkJoinPool pool = ForkJoinPool.commonPool(); // 使用公共的ForkJoinPool
- // 提交任务
- SumTask task = new SumTask(numbers, 0, numbers.length);
- Integer result = pool.invoke(task); // 阻塞当前线程直到任务完成
- System.out.println("Sum of numbers: " + result);
- // 注意:在大多数情况下,你不需要手动关闭ForkJoinPool,
- // 因为ForkJoinPool的公共实例是为了全局复用的。
- // 如果你创建了自己的ForkJoinPool实例,并且不再需要它,那么你应该调用shutdown()来关闭它。
- }
- }
复制代码 在这个示例中,SumTask类继承自RecursiveTask<Integer>,它表现一个返回Integer类型的递归任务。我们在compute方法中实现了任务的分割和归并逻辑。然后,在main方法中,我们创建了一个ForkJoinPool的公共实例,并提交了一个SumTask任务来计算一组数的和。末了,我们打印出了计算结果。
请留意,ForkJoinPool的公共实例(ForkJoinPool.commonPool())是为了全局复用的,因此通常不需要手动关闭它。如果你创建了自己的ForkJoinPool实例,那么在不再需要它时应该调用shutdown()方法来关闭它。
利用ForkJoinPool实现线程池的优缺点
长处:
- 工作窃取算法:ForkJoinPool采用工作窃取算法来均衡负载,这有助于淘汰线程空闲时间,提高资源利用率。当一个线程完成自己的任务后,它会从其他线程的队列中“窃取”任务来执行,从而保持线程忙碌状态。
- 专为分治算法筹划:ForkJoinPool特别适用于可以递归分解为较小任务的题目,如排序、搜索和大规模数据处理惩罚等。它允许任务在分解后并行执行,并在得当的时间归并结果。
- 灵活的并行性:开发者可以通过调解ForkJoinPool的并行度来控制同时执行的线程数量,以适应不同的硬件和负载环境。
- 简化编程模型:ForkJoinPool提供了一套简化的API,使得并行编程变得更加容易。开发者只需关注任务的分解和归并,而无需担心线程的管理和同步题目。
缺点:
- 任务分割开销:对于不适合分治算法的任务,大概任务分割的粒度太小,ForkJoinPool大概会因为任务分割和归并的开销而低落性能。
- 内存占用:由于ForkJoinPool中的任务大概会递归地创建更多的子任务,这大概会导致大量的内存占用,特别是在任务数量庞大或任务结构复杂的环境下。
- 任务依赖和同步:虽然ForkJoinPool提供了一些同步机制(如join()方法),但对于具有复杂依赖关系的任务,大概需要额外的同步步调来确保正确性,这大概会增加编程的复杂性。
- 学习曲线:对于不熟悉并行编程和ForkJoinPool的开发者来说,大概需要肯定的时间来学习和掌握其利用方法和最佳实践。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |