【问题标题】:Can a blocked thread be rescheduled to do other work?可以重新安排被阻塞的线程来做其他工作吗?
【发布时间】:2021-07-05 14:18:58
【问题描述】:

如果我有一个线程被阻塞等待锁定,操作系统是否可以重新安排该线程执行其他工作,直到锁定可用? 据我了解,它不能被重新安排,它只是闲置直到它可以获取锁。但这似乎效率低下。如果我们有 100 个任务提交给 ExecutorService,并且池中有 10 个线程:如果其中一个线程持有锁,而其他 9 个线程正在等待该锁,那么只有持有锁的线程才能取得进展。我原以为可以暂时重新安排被阻塞的线程来运行其他一些提交的任务。

【问题讨论】:

  • Java 线程和本机线程不是一回事。操作系统的调度程序可以使用本机线程做任何事情,而且它也对你的锁一无所知。我想你真正关心的可能是 Java 线程是如何实现的。
  • 你的意思是,理论上,本地线程可以被重新调度以运行其他任务,但是 Java 不是这样设计的并且阻止它发生?
  • 您是否对此类用例的解决方案感兴趣,或者您对 Java 可以做什么感兴趣?
  • @alfer hehe 你真的被困住了:P,基本上你又得到了同样的答案
  • @alfer 是的,这是真的 :),你很好奇这对程序员来说是个好主意

标签: java multithreading operating-system


【解决方案1】:

如果我有一个线程被阻塞等待锁定,操作系统是否可以重新安排该线程执行其他工作,直到锁定可用?据我了解,它不能被重新安排,它只是闲置直到它可以获取锁。但这似乎效率低下。

我认为您认为这完全是错误的。仅仅因为 20 个线程中有 10 个“空闲”并不意味着操作系统(或 JVM)以某种方式消耗资源来尝试管理这些空闲线程。虽然通常我们在应用程序上工作是为了确保我们的线程尽可能畅通无阻,以便我们能够实现最高吞吐量,但有很多次我们编写线程时我们希望它们大部分时间处于空闲状态。

如果我们有 100 个任务提交给 ExecutorService,并且池中有 10 个线程:如果其中一个线程持有锁,而其他 9 个线程正在等待该锁,那么只有持有锁的线程才能进行。我原以为可以暂时重新安排被阻塞的线程来运行其他一些提交的任务。

重新调度的不是线程,而是系统的CPU资源。如果 10 个线程中有 9 个在线程池中被阻塞,那么您的应用程序(垃圾收集器)或其他进程中的其他线程可以在您的服务器上获得更多 CPU 资源。这种工作之间的切换是现代操作系统真正擅长的,并且每秒会发生很多次。这都是很正常的。

现在,如果您的问题真的是“如何提高应用程序的吞吐量”,那么您问的问题是正确的。首先,您应该确保您的锁尽可能细粒度,以确保持有锁的线程在最短的时间内这样做。如果阻塞发生得太频繁,那么您应该考虑增加池中的线程数,以便某些作业并发运行的可能性更高。这种对线程池中线程数量的优化是非常特定于应用程序的。有关更多详细信息,请参阅我的帖子:Concept behind putting wait(),notify() methods in Object class

您可能会考虑的另一件事是将您的工作分解为多个部分,以将可以同时运行的部分与需要同步的部分分开。您可以有一个由 10 个线程组成的池来执行并发工作,然后一个线程执行需要锁的操作。这就是为什么编写ExecutorCompletionService 以便下游的东西可以获取线程池的结果并在它们完成时对其进行操作。这将使您的程序更加复杂,如果您正在谈论一些大量的作业或大量的结果,您将需要担心您的队列,但如果您做得正确,您可以显着提高吞吐量。

这种重构的一个很好的例子是您有一个处理作业,必须将结果写入数据库。如果在每个作业结束时,池中的每个线程都需要获得数据库连接上的锁,那么就会有很多锁竞争和更少的并发性。相反,如果处理是在线程池中完成的并且有一个数据库更新线程,它可以关闭自动提交并在提交之间连续从多个作业进行更新,这可以显着提高吞吐量。再说一次,使用由connection pool 管理的多个数据库连接可能是一个很好的解决方案。

【讨论】:

    【解决方案2】:

    你说:

    我原以为可以暂时重新安排被阻塞的线程来运行其他一些提交的任务。

    织机项目

    您碰巧正在描述作为Project Loom 的一部分开发的虚拟线程(纤程),用于Java 的未来版本。

    目前,Java 的 OpenJDK 实现使用来自主机操作系统的线程作为 Java 线程。所以这些线程的调度实际上是由操作系统而不是 JVM 控制的。是的,正如您所描述的,在所有常见的操作系统上,当 Java 代码阻塞时,代码的线程处于空闲状态。

    Project Loom 在“真实”平台/内核线程之上分层虚拟线程。许多虚拟线程可以映射到每个真实线程。在通用硬件上运行数百万个线程是可能的。

    使用 Loom 技术,JVM 可以检测阻塞代码。该阻塞代码的虚拟线程被“停放”,搁置一边,另一个虚拟线程分配给该真实线程以完成一些执行时间,而停放的线程等待响应。这种停车和切换非常迅速,开销很小。在 Loom 技术下,阻塞变得非常“便宜”。

    阻塞在大多数面向业务的普通应用程序中很常见。阻塞发生在文件 I/O、网络 I/O、数据库访问、日志记录、控制台交互、GUI 等等。通过 Project Loom 的实验性构建,此类使用虚拟线程的应用程序看到了巨大的性能提升。这些构建现在可用,基于早期访问 Java 17。Project Loom 团队寻求反馈。

    使用虚拟线程非常简单:切换您选择的执行器服务。

    ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
    

    警告:正如commented by Michael,JVM 管理的虚拟线程依赖于主机操作系统管理的平台/内核线程。最终,即使在 Loom 下,操作系统也会安排执行。当阻塞的 Java 线程在 CPU 内核上处于空闲状态时,虚拟线程非常有用。如果主机负载过重,Java 线程可能会看到很少的执行时间,不管有没有虚拟线程。

    虚拟线程不适合很少阻塞的任务,即真正的CPU-bound。例如,编码视频。此类任务应继续使用常规线程。

    有关详细信息,请参阅 Oracle 的 Ron Pressler 或 Loom 团队的其他成员的启发性演示和采访。随着 Loom 的发展,寻找最新的。

    【讨论】:

    • 感谢您的回复。为什么虚拟线程不适合真正受 CPU 限制的任务?
    • Project Loom 不会改变 JVM 与本地操作系统线程 WRT 调度的交互方式。 Loom 保留了几个本机线程作为虚拟线程的基础。操作系统仍然可以决定那些本机线程不应该获得 CPU 时间,如果它决定其他事情更重要的话。 OP 询问操作系统是否可以根据对哪些任务被阻塞的了解来决定如何安排某些任务。答案是否定的,在 Loom 下仍然会不会。
    • @alfer 虽然停放和切换虚拟线程非常便宜,但它并不是免费的。有一些开销。如果任务需要不间断地在 CPU 上持续工作,那么将线程移交给另一个任务是没有意义的。在挂起之前,应该允许 CPU 密集型任务有大量时间执行。平台/内核线程今天可以很好地处理这个问题。
    • Java 已经有了“重新调度”线程的工具;它被称为“fork/join 框架”。
    【解决方案3】:

    你说对了一部分。

    在你描述的执行器服务场景中,9个线程都将被阻塞,只有一个线程会进行。真的。

    您不太正确的部分是当您尝试期望 OS 和 Java 结合的行为时。看,线程的概念存在于操作系统和 Java 级别。但它们是两种不同的东西。所以有 Java-Threads 和 OS-Threads。 Java 线程通过 OS 线程实现

    这样想象一下,JVM 中有(比如说)10 个 Java 线程,有些正在运行,有些没有。 Java 借用了一些 OS-Thread 来实现正在运行的 Java-Threads。现在,当 Java-Thread 被阻塞(无论出于何种原因)时,我们可以确定的是 Java-Thread 已被阻塞。我们不能轻易观察底层 OS-Thread 发生了什么。

    操作系统可以回收操作系统线程并将其用于其他用途,或者它可以保持阻塞 - 这取决于。但是即使 OS-Thread 被重用,Java-Thread 仍然会被阻塞。在你的线程池场景中,9 个 Java 线程仍然会被阻塞,只有一个 Java 线程在工作。

    【讨论】:

    • 我认为至少对我来说最有用的思维模型是不假设 Java 线程和 OS 线程之间的任何特定映射。每个 Java 线程都可以由它自己的本地线程支持,或者所有 Java 线程都可以由同一个本地线程支持(因此根本不存在真正的并行性),或者介于两者之间。这是 JVM 的一个实现细节。
    • @Michael 这是我经常看到的常见错误
    • @Michael 哪些 Java 实现不会将每个 Java 线程映射到一个 OS 线程?
    • @BasilBourque 我从来没有说过有一个,尽管有几十个 JVM,包括用于嵌入式系统之类的目的,我当然不会排除它。我说从这些方面考虑它通常没有帮助,就像考虑将本机指令字节码映射到什么通常没有帮助一样。 JVM 提供了一种抽象,大多数时候将其视为一个黑盒子是很方便的。这就是抽象的意义所在。
    • @inquisitive 9 个 Java 线程将保持阻塞,但操作系统线程不会,所以如果我定义第二个线程池并向其提交任务,操作系统将能够启动它们吗?这也是我们在使用并行流的时候定义自定义forkjoinpools的原因,避免使用通用forkjoinpool的并行流过多而阻塞?
    【解决方案4】:

    我原以为可以暂时重新安排被阻塞的线程来运行其他一些提交的任务。

    这就是其他线程的用途。如果您创建 X 线程并且 Y 被阻塞,则您有剩余的 X-Y 线程来执行其他提交的任务。据推测,数字 X 是专门为获得实现和/或程序员认为最好的并发任务数而选择的。

    您在问为什么实施不忽略这个决定。答案是因为合理地选择线程数比让实现忽略该选择更有意义。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-27
      • 2019-11-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多