Java Top.

开始使用Spring 5和Spring Boot 2,通过学习春天课程:

>>查看课程

1.介绍

本文将介绍Java中的线程池—从标准Java库中的不同实现开始,然后介绍谷歌的Guava库。

进一步阅读:

Java中线程和虚拟线程的区别

Java中的线程和虚拟线程之间的快速实际比较。

Executorservice - 等待线程完成

学习如何在各种场景中使用ExecutorService来等待线程完成它们的执行。

Java 8并行流中的自定义线程池

简单介绍自定义线程池及其在Java 8并行流中的使用。

2.线程池

在Java中,线程映射到由操作系统资源的系统级线程。如果您无法控制地创建线程,您可能会快速耗尽这些资源。

线程之间的上下文切换也由操作系统来完成——以便模拟并行性。一种简单的观点是——生成的线程越多,每个线程用于实际工作的时间就越少。

线程池模式有助于在多线程应用程序中节省资源,并在某些预定义的限制中包含并行性。

当您使用线程池时,您以并行任务的形式编写并发代码,并将它们提交给线程池的实例执行.这个实例控制了几个可重用的线程来执行这些任务。
2016 - 08 - 10 - _10 - 16 - 52 - 1024 x572

这个模式允许你控制应用程序正在创建的线程数,他们的生命周期,以及安排任务的执行并在队列中保持传入的任务。

3. Java中的线程池

3.1。执行者执行者ExecutorService

执行者Helper类包含几个方法,用于为您创建预配置的线程池实例。这些类是开始的好地方——如果您不需要应用任何自定义微调,就使用它。

执行者ExecutorService接口用于使用Java中的不同线程池实现。通常,你应该使您的代码与线程池的实际实现分离并在整个应用程序中使用这些接口。

执行者接口具有单个执行提交方法可运行的实例的执行。

这是一个快速举例如何使用执行者api获得一个执行者实例,由单个线程池和无界队列支持,用于顺序地执行任务。这里,我们执行一个任务,它只输出"你好世界在屏幕上。该任务被作为lambda (Java 8特性)提交,这被推断为可运行的

Executor Executor = Executor . newsinglethreadexecutor ();executor.execute (() - > system . out。println(“Hello World”));

ExecutorService接口包含大量的方法控制任务的进度和管理服务的终止.使用这个接口,您可以提交要执行的任务,也可以使用返回的控件控制任务的执行未来实例。

在下面的例子中,我们创造了一个ExecutorService,提交一个任务,然后使用返回的未来s得到方法等待提交的任务完成并返回该值:

ExecutorService ExecutorService = Executors.newFixedThreadPool(10);Future Future = executorService.submit(() ->“Hello World”);//一些操作String result = future.get();

当然,在现实生活中,您通常不想打电话future.get ()立即但延迟调用它,直到您实际需要计算值。

提交方法重载以接受其中之一可运行的可谴责这两者都是功能接口,可以作为lambdas(以Java 8开始)传递。

可运行的单个方法不会抛出异常,并且不会返回值。的可谴责接口可能更方便,因为它允许我们抛出异常并返回一个值。

最后——让编译器推断可谴责类型,只需返回Lambda的值。

属性的更多示例ExecutorService界面和未来,看看"Java ExecutorService指南“。

3.2。ThreadPoolExecutor

ThreadPoolExecutor是一个可扩展的线程池实现,具有大量参数和用于微调的钩子。

我们将在这里讨论的主要配置参数是:corePoolSizemaximumPoolSize, 和KeepAliveTime.

池由固定数量的始终保持在内部的核心线程和一些多余的线程组成,这些线程可以派生,然后在不再需要它们时终止。的corePoolSize参数是将被实例化并保存在池中的核心线程的数量。当一个新任务出现时,如果所有核心线程都忙碌并且内部队列已满,则允许池中长大maximumPoolSize

KeepAliveTime.参数是过量线程(实例化的线程超过corePoolSize)被允许存在于空闲状态。默认情况下,ThreadPoolExecutor只考虑非核心线程的删除。为了将相同的删除策略应用于核心线程,我们可以使用allowCoreThreadTimeOut(真正的)方法。

这些参数涵盖了各种用例,但最典型的配置是预定义的执行者静态方法

例如newFixedThreadPool方法创建一个ThreadPoolExecutor平等corePoolSizemaximumPoolSize参数值和零KeepAliveTime。这意味着线程池中的线程数总是相同的:

ThreadPoolExecutor = (ThreadPoolExecutor) executor . newfixedthreadpool (2);execute .submit(() -> {Thread.sleep(1000);返回null;});execute .submit(() -> {Thread.sleep(1000);返回null;});execute .submit(() -> {Thread.sleep(1000);返回null;}); assertEquals(2, executor.getPoolSize()); assertEquals(1, executor.getQueue().size());

在上面的例子中,我们实例化了一个ThreadPoolExecutor固定螺纹数为2。这意味着如果同时运行的任务数量始终小于或等于两个,那么它们将立即执行。否则,其中一些任务可以放入队列中等待轮到它们

我们创建了三个可谴责通过睡眠1000毫秒来模仿繁重的工作的任务。前两个任务将立即执行,第三个任务必须在队列中等待。我们可以通过致电来验证它getPoolSize ().size getQueue () ()方法后立即提交任务。

另一个预先配置ThreadPoolExecutor可以用Executors.newCachedThreadPool ()方法。此方法根本不会收到许多线程。的corePoolSize实际上设置为0,而maximumPoolSize被设置为integer.max_value.对于这个实例。的KeepAliveTime.这是60秒。

这些参数值意味着缓存的线程池可以无限制地增长以容纳任何数量的提交任务.但是当线程不再需要时,它们将在60秒不活动后被处理。一个典型的用例是当您的应用程序中有许多短期任务时。

ThreadPoolExecutor = (ThreadPoolExecutor) executor . newcachedthreadpool ();execute .submit(() -> {Thread.sleep(1000);返回null;});execute .submit(() -> {Thread.sleep(1000);返回null;});execute .submit(() -> {Thread.sleep(1000);返回null;}); assertEquals(3, executor.getPoolSize()); assertEquals(0, executor.getQueue().size());

上面例子中的队列大小将始终为零,因为内部aSynchronousueue.使用实例。在一个Synchronousueue.,对插入删除操作总是同时发生,因此队列实际上从不包含任何内容。

Executors.newSingleThreadExecutor ()API创建了另一种典型形式ThreadPoolExecutor包含单个线程的。单程执行器非常适合创建事件循环。corePoolSizemaximumPoolSize参数等于1,而KeepAliveTime.是零。

上面的例子中的任务会顺序执行,所以在任务完成后,flag值为2:

atomicinteger计数器= new oromicinteger();ExecutorService Executor = Executors.newsingLethReadexecutor();executor.submit(() - > {counter.set(1);});executor.submit(() - > {counter.compareandset(1,2);});

此外,这ThreadPoolExecutor使用不可变包装器进行装饰,因此在创建后不能重新配置。请注意,这也是不能将其强制转换为ThreadPoolExecutor

3.3。ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor扩展了ThreadPoolExecutor类,并实现ScheduledExecutorService与几种附加方法的接口:

  • 时间表方法允许在指定的延迟后执行任务一次;
  • scheduleatfixedrate.方法允许在指定的初始延迟后执行任务,然后在一定时间内重复执行该任务;的时期论证是时候了在任务的开始时间之间测量,所以执行率是固定的;
  • scheduleWithFixedDelay方法类似于scheduleatfixedrate.在它中,它反复执行给定的任务,但指定的延迟是衡量的是前一个任务的结束和下一个任务的开始;执行速度可能会根据执行任何给定任务所需的时间而变化。

executors.newscheduledthreadpool()方法通常用于创建ScheduledThreadPoolExecutor与给定corePoolSize,无限maximumPoolSize和零KeepAliveTime..以下是如何在500毫秒中安排执行任务:

ScheduledExecutorService executor = executors . newschedulethreadpool (5);执行器.schedule(() ->{系统退出。println(“Hello World”);500年},TimeUnit.MILLISECONDS);

下面的代码展示了如何在延迟500毫秒后执行任务,然后每100毫秒重复一次。调度任务之后,我们使用CountDownLatch然后使用Future.cancel ()方法。

countdownlatch lock = new countdownlatch(3);ScheduledExecutorService executor = executors . newschedulethreadpool (5);scheduledfuture <?>未来= executor.scheduleatfixedrate(() - > {system.out.println(“hello world”); lock.countdown();},500,100,timeUnit.milliseconds);lock.await(1000,TimeUnit.milliseconds);未来.Cancel(真实);

3.4。ForkJoinPool

ForkJoinPool是中央部分叉/加入Java 7中引入的框架。它解决了一个常见的问题在递归算法中生成多个任务.使用一个简单的ThreadPoolExecutor,您将很快耗尽线程,因为每个任务或子任务都需要自己的线程来运行。

在一个叉/加入框架,任何任务都可以派生(),并使用加入方法。福利叉/加入框架就是它不为每个任务或子任务创建新线程,实现工作窃取算法。本框架在文章中彻底描述了“Java Fork/Join框架指南

让我们看一个简单的使用示例ForkJoinPool遍历一棵节点树并计算所有叶子值的和。下面是一个由节点组成的树的简单实现int值和一组子节点:

静态类TreeNode {int值;SET 儿童;treeenode(int值,treeNode ...儿童){this.value =值;this.children = set.newhashset(儿童);}}

现在如果我们想要并行地对树中的所有值求和,我们需要实现RecursiveTask <整数>接口。每个任务接收它自己的节点,并将它的值与它的值之和相加孩子们.计算…的和孩子们值,任务执行做以下:

  • 流的孩子们集,
  • 映射到此流上,创建一个新的countingtask.对于每个元素,
  • 通过分叉来执行每个子批次,
  • 调用加入方法对每个分叉任务,
  • 对结果进行求和收藏家.Summingint.集电极。
public static class CountingTask extends RecursiveTask {private final TreeNode node;public CountingTask(TreeNode node) {this。节点=节点;} @Override protected Integer compute(){返回节点。value + node.children.stream() .map(childNode -> new CountingTask(childNode).fork()) .collect(collators . summingint (ForkJoinTask::join));}}

要在实际树上运行计算的代码非常简单:

TreeNode Tree =新的TreeNode(5,New TrieNode(3),新的TreeNode(2,新的TreeNode(2),新的TreeNode(8)));forkjoinpool forkjoinpool = forkjoinpool.commonpool();INT = forkjoinpool.invoke(新的countingtask(树));

4.线程池的实现在番石榴

番石榴是一个流行的谷歌实用程序库。它有许多有用的并发类,包括几个方便的ExecutorService.不能直接实例化或子类化实现类,因此创建它们的实例的唯一入口点是MoreExecutors助手类。

4.1。添加番石榴作为Maven依赖项

将以下依赖项添加到Maven pom文件中,以将番石榴库包含到项目中。您可以找到最新版本的番石榴库Maven Central.存储库:

<依赖> < groupId > com.google。 19.0 

4.2。直接执行者和直接执行者服务

有时您希望在当前线程或线程池中执行任务,这取决于某些条件。您更喜欢使用单个执行者接口,然后切换实现。虽然要想出一个实现并不难执行者ExecutorService执行当前线程中的任务,它仍然需要编写一些样板代码。

很高兴,番石榴为我们提供了预定义的实例。

这里有一个例子这演示了一个任务在同一个线程中的执行。虽然提供的任务休眠500毫秒,但它阻塞当前线程,测试结束后,结果立即可用执行调用完成:

Executor Executor = MoreExecutors.directExecutor();AtomicBoolean执行= new AtomicBoolean();execute .execute(() -> {try {Thread.sleep(500);} catch (InterruptedException e) {e. printstacktrace ();} executed.set(真正的);});assertTrue (executed.get ());

对象返回的实例DirectExecutor()方法实际上是一个静态单例,所以使用此方法并不在对象创建时提供任何开销。

你应该更喜欢这种方法MoreExecutors.newDirectExecutorService ()因为该API在每个调用上都创建了一个完整的执行器服务实现。

4.3。退出执行者服务

另一个常见的问题是关闭虚拟机虽然线程池仍在运行任务。即使有取消机制,也无法保证,当执行者服务关闭时,任务将很好地表现并停止工作。这可能导致JVM无限期地挂起,而任务继续执行工作。

为了解决这个问题,Guava介绍了一系列退出的执行者服务。他们是基于的与JVM一起终止的守护线程

这些服务还添加了一个关闭挂钩.addShutdownHook Runtime.getRuntime () ()方法并防止VM在放弃挂起任务之前终止配置的时间量。

在下面的示例中,我们提交的任务包含一个无限循环,但是我们使用一个已退出的执行器服务,它的配置时间为100毫秒,以等待VM终止时的任务。没有exitingExecutorService,该任务将导致VM无限期挂起:

ThreadPoolExecutor = (ThreadPoolExecutor) executor . newfixedthreadpool (5);ExecutorService ExecutorService = MoreExecutors。得到ExitingExecutorService(executor, 100, TimeUnit.MILLISECONDS); executorService.submit(() -> { while (true) { } });

4.4。听修饰符

侦听装饰器允许您包装ExecutorService和接收听听核实实例提交任务而不是简单未来实例。的听听核实接口扩展未来并且有一个额外的方法addListener.这个方法允许添加一个监听器,在将来完成时调用它。

你很少想要使用ListenableFuture.addListener ()方法,但它是类中的大多数帮助器方法所必需的期货效用类.例如,与Futures.allAsList ()方法,您可以组合多个听听核实单一的实例听听核实在成功完成所有期货组合后即完成:

ExecutorService ExecutorService = Executors.newCachedThreadPool();ListeningExecutorService ListeningExecutorService = MoreExecutors.listeningDecorator(executorService);ListenableFuture future1 = listeningExecutorService.submit(() ->“Hello”);ListenableFuture future2 = listeningExecutorService.submit(() ->“World”);字符串问候语=未来。allAsList(future1, future2).get() .stream() .collect(收藏家。加入(" "));assertequal(“Hello World”,问候);

结论

在本文中,我们讨论了线程池模式及其在标准Java库和谷歌的番石榴库中的实现。

可以使用文章的源代码在GitHub

Java底部

开始使用Spring 5和Spring Boot 2,通过学习春天课程:

>>查看课程
2评论
最古老的
最新的
内联反馈
查看所有评论
对这篇文章的评论关闭!