【问题标题】:Concurrency within Java EE environmentJava EE 环境中的并发性
【发布时间】:2023-03-18 02:24:01
【问题描述】:

目标

我的目标是更好地了解 Java EE 环境中的并发性以及如何更好地使用它。

一般问题

我们以典型的 servlet 容器(tomcat)为例。对于每个请求,它使用 1 个线程来处理它。线程池配置为,池中最多可以有 80 个线程。让我们也以简单的 webapp 为例 - 它在每个请求期间进行一些处理和数据库通信。

在高峰时间,我可以看到 80 个并行运行的线程(+ 几个其他基础设施线程)。我们还假设我在“m1.large”EC2 实例中运行它。

我不认为所有这些线程都可以真正在这个硬件上并行运行。所以现在调度程序应该决定如何更好地在它们之间分配 CPU 时间。所以问题是 - 在这种情况下调度程序开销有多大?如何在线程数量和处理速度之间找到合适的平衡点?

演员比较

在 4 核 CPU 上拥有 80 多个线程对我来说听起来并不健康。特别是如果它们中的大多数被某种 IO(数据库、文件系统、套接字)阻塞 - 它们只会消耗宝贵的资源。如果我们将请求从线程中分离出来,并且只有合理数量的线程(例如 8 个)并且只向它们发送处理任务,该怎么办?当然,在这种情况下,IO 也应该是非阻塞的,这样当我需要的一些数据可用时我会接收事件,如果我有一些结果,我会发送事件。

据我了解,Actor 模型就是这样。 Actor 不绑定到线程(至少在 Akka 和 Scala 中)。所以我有合理的线程池和一堆包含处理任务的邮箱的actor。

现在的问题是 - actor 模型在性能、调度程序开销和资源(RAM、CPU)消耗方面与传统的每请求线程模型相比如何?

自定义线程

我有一些请求(只有几个)需要花费太多时间来处理。我优化了代码和所有算法,添加了缓存,但仍然需要太多时间。但我明白了,该算法可以并行化。它很自然地适合演员模型——我只是将我的大任务分成几个任务,然后以某种方式聚合结果(如果需要)。但是在每请求线程模型中,我需要生成自己的线程(或创建我的小线程池)。据我所知,在 Java EE 环境中不建议这样做。而且,从我的角度来看,它并不自然地适合每个请求的线程模型。问题出现了:我的线程池大小应该有多大?即使我会在硬件方面使其合理,我仍然有这堆由 servlet 容器管理的线程。线程管理变得分散并变得疯狂。

所以我的问题 - 在每个请求线程模型中处理这些情况的最佳方法是什么?

【问题讨论】:

    标签: concurrency jakarta-ee httprequest actor


    【解决方案1】:

    在 4 核 CPU 上拥有 80 多个线程对我来说听起来并不健康。特别是如果它们中的大多数被某种 IO(数据库、文件系统、套接字)阻塞 - 它们只会消耗宝贵的资源。

    错了。正是在这种情况下,处理器可以处理比单个内核数量更多的线程,因为大多数线程在任何时间点都被阻塞等待 I/O。很公平,上下文切换需要时间,但与文件/网络/数据库延迟相比,这种开销通常无关紧要。

    线程数应等于(或略多于)处理器内核数的经验法则仅适用于计算密集型任务,此时内核大部分时间都处于忙碌状态。

    我有一些请求(只有几个)需要花费太多时间来处理。我优化了代码和所有算法,添加了缓存,但仍然需要太多时间。但我明白了,该算法可以并行化。它很自然地适合演员模型——我只是将我的大任务分成几个任务,然后以某种方式聚合结果(如果需要)。但是在每个请求线程模型中,我需要生成自己的线程(或创建我的小线程池)。据我所知,不建议在 Java EE 环境中这样做。

    从未听说过(但我并不声称自己是最终的 Java EE 专家)。恕我直言,使用例如并行执行与单个请求相关的任务没有错。 ThreadPoolExecutor。请注意,这些线程不是请求处理线程,因此它们不会直接干扰 EJB 容器使用的线程池。除了它们当然会争夺相同的资源,因此它们可能会在粗心的设置中减慢或完全停止其他请求处理线程。

    在每个请求线程模型中处理这些情况的最佳方法是什么?

    最后,您无法避免测量并发性能并针对您自己的特定环境微调线程池的大小和其他参数。

    【讨论】:

      【解决方案2】:

      Java EE 的重点是将安全、状态和并发等常见架构问题放入框架中,并让您提供业务逻辑或数据映射以及连接它们的布线。因此,Java EE 有意在框架中隐藏了令人讨厌的并发性(锁定到读/写可变状态)。

      这种方法可以让更广泛的开发人员成功编写正确的应用程序。但是,一个必要的副作用是这些抽象会产生开销并移除控制。这既是好的(简化并将策略编码为策略而不是代码)也不好(如果您知道自己在做什么并且可以在框架中做出不可能的选择)。

      在生产盒上拥有 80 个线程本质上并不坏。大多数将被阻塞或等待 I/O,这很好。有一个(可调)线程池进行实际计算,Java EE 会为您提供外部挂钩来调整这些旋钮。

      演员是不同的模型。它们还允许您编写代码孤岛(演员主体),(可以)避免锁定以修改状态。您可以将您的参与者编写为无状态(在递归函数调用参数中捕获状态)或将您的状态完全隐藏在参与者实例中,以便状态全部受限(对于反应式参与者,您可能仍然需要明确锁定数据访问权限确保在运行您的演员的下一个线程上的可见性)。

      我不能说哪个更好。我认为有充分的证据表明这两种模型都可以用来编写安全的高吞吐量系统。为了使两者都表现良好,您需要认真思考您的问题并构建应用程序来隔离部分状态和每种状态的计算。对于您能很好地理解数据并具有很高的并行潜力的代码,我认为 Java EE 之外的模型很有意义。

      通常,调整计算绑定线程池大小的经验法则是,它们应该大约等于内核数 + 2。许多框架会自动调整大小。您可以使用 Runtime.getRuntime().availableProcessors() 来获取 N。如果您的问题分解为分而治之的算法并且数据项的数量很大,我强烈建议您检查 fork/join 可以现在用作一个单独的库,并将成为 Java 7 的一部分。

      至于如何管理这一点,您不应该在 Java EE 中生成这样的线程(他们想要控制它),但您可能会调查通过消息队列向您的数据处理线程池发送请求和通过返回消息处理该请求。这可以适应 Java EE 模型(当然有点笨拙)。

      我在这里写了一个关于actors、fork/join 和其他一些您可能会感兴趣的并发模型的文章:http://tech.puredanger.com/2011/01/14/comparing-concurrent-frameworks/

      【讨论】:

      • "(对于 React 风格的 Actor,您可能仍需要显式锁定数据访问以确保在运行您的 Actor 的下一个线程上的可见性)。"
      • @Viktor:嗯,你会知道的,所以我相信你!我正在设想一个具有私有可变状态的 Scala 演员的情况。在这种情况下,必须有人确保 JVM 级别的可见性障碍,对吧?谁?
      • @Alex 通常只是将消息执行包装在一个同步块中,因为这是单点入口,它提供了所需的同步/刷新
      • @Viktor:嗯,使用同步块正是我所说的“显式锁定数据访问”的意思......
      • @Alex:我将“对于 react 风格的演员,你可能仍然需要明确锁定数据访问”中的“你”解释为“框架的用户”,而不是框架的实现者。所以,澄清一下,用户不需要围绕它做任何显式锁,锁是由框架隐式提供的,由实施者。
      猜你喜欢
      • 1970-01-01
      • 2017-12-24
      • 2013-09-05
      • 1970-01-01
      • 2010-10-24
      • 1970-01-01
      • 2015-06-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多