Java线程池和Guava创建线程池详解

1.简介

本文介绍了Java中的线程池-从标准Java库中的不同实现开始,然后介绍Google的Guava库。

2.线程池

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

为了模拟并行性线程之间的上下文切换也由操作系统完成。一种简单的观点是:产生的线程越多,每个线程花费在实际工作上的时间就越少。

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

使用线程池时,您以并行任务的形式编写并发代码,并将其提交给线程池的实例执行。该实例控制几个重复使用的线程来执行这些任务。
在这里插入图片描述

该模式使您可以控制应用程序正在创建的线程数,它们的生命周期,以及计划任务的执行并将传入的任务保持在队列中。

3. Java中的线程池

3.1 Executors, Executor 和 ExecutorService

Executors辅助类包含用于创建为你预先配置的线程池实例的几种方法。这些类是一个很好的起点,如果您不需要应用任何自定义的微调,使用它就完全可以。

Executor 和 ExecutorService接口用于与Java中不同的线程池实现的工作。通常,您应该**使代码与线程池的实际实现抽象接口,**并在整个应用程序中使用这些接口。

该执行器接口只有一个执行方法提交的Runnable实例执行。

这是一个简单的示例,说明如何使用Executors API获取由单个线程池和无限制队列支持的Executor实例,以按顺序执行任务。在这里,我们执行一个仅在屏幕上打印“ Hello World ”的任务。任务以lambda(Java 8功能)提交,可以推断为Runnable

Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Hello World"));

ExecutorService的界面包含了大量的方法控制任务的进度和管理服务的终止。使用此接口,您可以提交任务以执行,也可以使用返回的Future实例控制任务的执行。

在下面的示例中,我们创建一个ExecutorService,提交一个任务,然后使用返回的Futureget方法等待直到提交的任务完成并返回值:

ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<String> future = executorService.submit(() -> "Hello World");
// some operations
String result = future.get();

当然,在现实生活中,您通常不希望立即调用future.get(),而是推迟调用它,直到您真正需要计算值为止。

所述提交方法被重载采取任何可运行可赎回这两者都是功能接口,并且可以作为lambda表达式被传递(与Java 8开始)。

Runnable的单个方法不会引发异常,也不返回值。该可调用接口可能会更方便,因为它允许我们抛出一个异常,并返回一个值。

最后,要让编译器推断Callable类型,只需从lambda返回一个值。

3.2 线程池执行器

ThreadPoolExecutor的是,有很多的参数和挂钩微调一个可扩展的线程池实现。

我们将在这里讨论的主要配置参数是:corePoolSizemaximumPoolSizekeepAliveTime

该池由固定数量的核心线程组成,这些线程始终保持在内部,并且某些多余的线程可能会在不再需要时产生并随后终止。该corePoolSize参数是将被实例化,并保持在池中核心线程数。当出现新任务时,如果所有核心线程都忙并且内部队列已满,则允许池增长到maximumPoolSize

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

这些参数涵盖了广泛的用例,但是最典型的配置是在Executors静态方法中预定义的

例如newFixedThreadPool方法创建一个corePoolSizemaximumPoolSize参数值相等且keepAliveTime为零的ThreadPoolExecutor *。*这意味着该线程池中的线程数始终相同:

ThreadPoolExecutor executor = 
  (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});

assertEquals(2, executor.getPoolSize());
assertEquals(1, executor.getQueue().size());

在上面的示例中,我们实例化了一个具有固定线程数2的ThreadPoolExecutor。这意味着,如果同时运行的任务数量始终小于或等于两个,则它们将立即执行。否则,其中一些任务可能会排队等待轮到他们

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

可以使用Executors.newCachedThreadPool() 方法创建另一个预配置的ThreadPoolExecutor*。此方法根本不接收多个线程。此实例的corePoolSize实际上设置为0,maximumPoolSize设置为Integer.MAX_VALUE。该时间的*keepAliveTime为60秒。

这些参数值表示高速缓存的线程池可能会无限增长,以容纳任意数量的已提交任务。但是,当不再需要线程时,将在60秒钟不活动之后将其丢弃。一个典型的用例是您的应用程序中有很多短期任务。

ThreadPoolExecutor executor = 
  (ThreadPoolExecutor) Executors.newCachedThreadPool();
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});
executor.submit(() -> {
    Thread.sleep(1000);
    return null;
});

assertEquals(3, executor.getPoolSize());
assertEquals(0, executor.getQueue().size());

上面的示例中的队列大小将始终为零,因为内部使用了SynchronousQueue实例。在SynchronousQueue中,成对的插入删除操作始终同时发生,因此队列实际上从不包含任何内容。

所述Executors.newSingleThreadExecutor() API创建的另一典型形式的ThreadPoolExecutor含有单个线程。**单线程执行程序是创建事件循环的理想选择。**的corePoolSizemaximumPoolSize参数等于1,并且KeepAliveTime的是零。

上例中的任务将按顺序执行,因此任务完成后标志值将为2:

AtomicInteger counter = new AtomicInteger();

ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
    counter.set(1);
});
executor.submit(() -> {
    counter.compareAndSet(1, 2);
});

此外,此ThreadPoolExecutor装饰有不可变的包装,因此在创建后无法重新配置。请注意,这也是我们无法将其强制转换为ThreadPoolExecutor的原因

3.3。ScheduledThreadPoolExecutor

的ScheduledThreadPoolExecutor扩展的ThreadPoolExecutor类,也实现了ScheduledExecutorService的几个额外的方法接口:

  • schedule方法允许在指定的延迟后执行一次任务;
  • scheduleAtFixedRate方法允许在指定的初始延迟后执行任务,然后在特定时间段内重复执行;该周期参数是时间的任务的开始时间之间测量的,所以执行速率是固定的;
  • scheduleWithFixedDelay方法与scheduleAtFixedRate相似,因为它重复执行给定的任务,但是指定的延迟是在上一个任务的结束与下一个任务的开始之间进行测量的;执行速度可能取决于执行任何给定任务所花费的时间。

所述Executors.newScheduledThreadPool()方法通常用于创建的ScheduledThreadPoolExecutor与给定corePoolSize,无界maximumPoolSize和零KeepAliveTime的。以下是安排任务在500毫秒内执行的方法:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
executor.schedule(() -> {
    System.out.println("Hello World");
}, 500, TimeUnit.MILLISECONDS);

以下代码显示了如何在延迟500毫秒后执行任务,然后每100毫秒重复一次。计划任务之后,我们等待直到使用CountDownLatch锁将其激发3次*,然后使用Future.cancel()*方法将其取消。

CountDownLatch lock = new CountDownLatch(3);

ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
ScheduledFuture<?> future = executor.scheduleAtFixedRate(() -> {
    System.out.println("Hello World");
    lock.countDown();
}, 500, 100, TimeUnit.MILLISECONDS);

lock.await(1000, TimeUnit.MILLISECONDS);
future.cancel(true);

3.4 分流池

ForkJoinPool是Java 7中引入的fork / join框架的核心部分。它解决了递归算法产生多个任务的常见问题。使用简单的ThreadPoolExecutor,您将很快用完线程,因为每个任务或子任务都需要自己的线程才能运行。

fork / join框架中,任何任务都可以生成(fork)许多子任务,并使用join方法等待其完成。fork / join框架的优点在于,它**不会为每个任务或子任务创建新线程,**而是实现了Work Stealing算法。

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

static class TreeNode {

    int value;

    Set<TreeNode> children;

    TreeNode(int value, TreeNode... children) {
        this.value = value;
        this.children = Sets.newHashSet(children);
    }
}

现在,如果要对树中的所有值进行并行求和,则需要实现RecursiveTask 接口。每个任务接收其自己的节点,并增加其价值,它的价值的总和孩子。要计算子代值的总和,任务实现将执行以下操作:

  • children建立strem流
  • 映射到此流,为每个元素创建一个新的CountingTask
  • 通过分叉来执行每个子任务
  • 通过对每个分叉任务调用join方法来收集结果,
  • 使用Collectors.summingInt收集器对结果求和。
public static class CountingTask extends RecursiveTask<Integer> {

    private final TreeNode node;

    public CountingTask(TreeNode node) {
        this.node = node;
    }

    @Override
    protected Integer compute() {
        return node.value + node.children.stream()
          .map(childNode -> new CountingTask(childNode).fork())
          .collect(Collectors.summingInt(ForkJoinTask::join));
    }
}

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

TreeNode tree = new TreeNode(5,
  new TreeNode(3), new TreeNode(2,
    new TreeNode(2), new TreeNode(8)));

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
int sum = forkJoinPool.invoke(new CountingTask(tree));

4.线程池在Guava中的实现

Guava 是流行的Google工具库。它具有许多有用的并发类,包括ExecutorService的一些便捷实现。实现类无法直接实例化或子类访问,因此创建其实例的唯一入口点是MoreExecutors帮助器类。

4.1 将Guava添加为Maven依赖项

将以下依赖项添加到您的Maven pom文件中,以将Guava库包括到您的项目中。您可以在Maven Central存储库中找到最新版本的Guava库:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>19.0</version>
</dependency>

4.2 直接Executor 和直接 Executor Service

有时,您需要根据某些条件在当前线程或线程池中执行任务。您可能希望使用单个Executor接口,而只是切换实现。尽管提出执行当前线程中的任务的ExecutorExecutorService的实现并不难,但仍需要编写一些样板代码。

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

这是一个示例,演示在同一线程中任务的执行。尽管所提供的任务休眠500毫秒,但它阻塞了当前线程,并且在execute调用完成后立即可以得到结果:

Executor executor = MoreExecutors.directExecutor();

AtomicBoolean executed = new AtomicBoolean();

executor.execute(() -> {
    try {
        Thread.sleep(500);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    executed.set(true);
});

assertTrue(executed.get());

*directExecutor()*方法返回的实例实际上是一个静态单例,因此使用此方法根本不会为对象创建提供任何开销。

您应该首选此方法,而*不是MoreExecutors.newDirectExecutorService(),*因为该API在每次调用时都会创建完整的执行程序服务实现。

4.3 退出执行器服务

另一个常见问题是在线程池仍在运行其任务时关闭虚拟机。即使有取消机制,也不能保证执行程序服务关闭时任务会表现良好并停止工作。这可能会导致JVM在任务继续工作时无限期挂起。

为了解决这个问题,Guava引入了一系列退出执行器服务。它们基于与JVM一起终止的守护程序线程

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

在以下示例中,我们要提交包含无限循环的任务,但是我们使用配置了100毫秒的退出执行程序服务来等待VM终止时执行任务。没有exitingExecutorService,此任务将导致VM无限期挂起:

ThreadPoolExecutor executor = 
  (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
ExecutorService executorService = 
  MoreExecutors.getExitingExecutorService(executor, 
    100, TimeUnit.MILLISECONDS);

executorService.submit(() -> {
    while (true) {
    }
});

4.4 监听器

监听器您可以包装ExecutorService并在提交任务时接收ListenableFuture实例,而不是简单的Future实例。所述ListenableFuture接口扩展未来,并且具有单个附加方法的addListener。此方法允许添加在将来完成时调用的监听器。

您几乎不需要直接使用ListenableFuture.addListener()方法,但是对于Futures实用程序类中的大多数帮助器方法来说,它是必不可少的。例如,使用Futures.allAsList()方法,您可以将多个ListenableFuture实例合并到一个ListenableFuture中,该实例在成功完成所有合并的期货后才完成:

ExecutorService executorService = Executors.newCachedThreadPool();
ListeningExecutorService listeningExecutorService = 
  MoreExecutors.listeningDecorator(executorService);

ListenableFuture<String> future1 = 
  listeningExecutorService.submit(() -> "Hello");
ListenableFuture<String> future2 = 
  listeningExecutorService.submit(() -> "World");

String greeting = Futures.allAsList(future1, future2).get()
  .stream()
  .collect(Collectors.joining(" "));
assertEquals("Hello World", greeting);

5.结论

在本文中,我们讨论了标准Java库和Google的Guava库中的线程池模式及其实现。

  • 本文部分内容参考并翻译了国外博客内容
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员麻薯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值