Java线程池,看这个就够了

1.简介

本文将介绍Java中的线程池 - 从标准Ja​​va库中的不同实现开始,然后查看Google的Guava库。

2.线程池

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

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

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

使用线程池时,以并行任务的形式编写并发代码,并将它们提交给线程池的实例执行。此实例控制多个重用线程以执行这些任务。
image.png

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

3. Java中的线程池

3.1 Executors,Executor和ExecutorService

该执行人辅助类包含了创建为你预先配置的线程池实例的几种方法。这些课程是一个很好的开始 - 如果您不需要应用任何自定义微调,请使用它。

该Executor 及ExecutorService接口用于在Java中不同的线程池实现一起工作。通常,您应该将代码与线程池的实际实现分离,并在整个应用程序中使用这些接口。

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

下面是一个快速示例,说明如何使用Executors API获取由单个线程池和无限队列支持的Executor实例,以便按顺序执行任务。在这里,我们执行一个只在屏幕上打印“ Hello World ”的任务。该任务以lambda(Java 8特性)提交,指向为Runnable。

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

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

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

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

当然,在现实场景中,您通常不希望立即调用future.get( ),而是推迟调用它直到您实际需要计算值。

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

Runnable的单个方法不会抛出异常并且不返回值。可调用接口可能更方便,因为它允许抛出异常并返回值。

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

3.2 ThreadPoolExecutor

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

我们将在这里讨论的主要配置参数是:corePoolSize,maximumPoolSize和keepAliveTime。

该池由固定数量的核心线程组成,这些线程始终保持在内部,还有一些过多的线程可能会被生成,然后在不再需要时终止。所述corePoolSize参数是将被实例化并保持在核心数量。如果所有核心线程都忙,并且提交了更多任务,则允许池增长到maximumPoolSize。

该KeepAliveTime的参数是的量,过量的线程(即在过量的实例,即线程时间间隔corePoolSize)被允许在空闲状态存在。

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

例如,newFixedThreadPool方法创建一个ThreadPoolExecutor,它具有相等的corePoolSize和maximumPoolSize参数值以及零keepAliveTime。这意味着此线程池中的线程数始终相同:

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。这意味着如果同时运行的任务的数量始终小于或等于2,那么它们会立即执行。否则,其中一些任务可能会被放入队列中等待轮到他们。

我们创建了三个可调用任务,通过睡眠模拟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含有单个线程。单线程执行程序是创建事件循环的理想选择。corePoolSize和maximumPoolSize参数等于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锁触发三次,然后使用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

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 接口。每个任务都接收自己的节点,并将其值添加到其子节点的值之和。要计算子值的总和,任务实现执行以下操作:

  • 将子集合转化为流
  • 在这个流上映射,为每个元素创建一个新的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和ExecutorService

有时您希望在当前线程或线程池中执行任务,具体取决于某些条件。您更愿意使用单个Executor接口,只需切换实现。虽然提出执行当前线程中的任务的Executor或ExecutorService的实现并不困难,但它仍然需要编写一些模板代码。

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

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

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库中的实现。
image

欢迎大家关注公众号:「Java知己」,关注公众号,回复「1024」你懂得,免费领取 30 本经典编程书籍。关注我,与 10 万程序员一起进步。
每天更新Java知识哦,期待你的到来!

image

猜你喜欢

转载自blog.csdn.net/feilang00/article/details/87255961