被阻塞的线程能否被重新调度以执行其他任务?

4
如果我的线程因为等待锁而被阻塞,操作系统能否重新安排该线程执行其他工作,直到锁变得可用? 据我了解,它无法被重新安排,只会一直处于空闲状态,直到它可以获得锁。但这似乎效率很低。如果我们向ExecutorService提交了100个任务,并且线程池中有10个线程:如果其中一个线程持有锁,另外9个线程在等待该锁,那么只有持有锁的线程才能取得进展。我本来以为被阻塞的线程可以被暂时重新安排以运行其他提交的任务。

Java线程和本地线程不是同一回事。操作系统的调度程序可以随意处理本地线程,并且它对您的锁一无所知。我认为您实际关心的可能是Java线程的实现方式。 - Michael
我只想了解Java的工作原理,我并不特别寻求一种解决方案。 - alfer
1
@alfer 嘿嘿,你真的被这个问题卡住了 :P,基本上你又得到了相同的答案。 - dreamcrash
@dreamcrash 确实,你先给了我答案,谢谢。但是通过阅读这里的其他答案,我也学到了其他有趣的东西。 - alfer
1
@alfer 是的,那是真的:),作为程序员,好奇心是一件好事。 - dreamcrash
显示剩余2条评论
4个回答

5
您说:
我本以为被阻塞的线程可以临时重新安排运行其他已提交的任务。
项目织物
您所描述的正是正在开发中的虚拟线程(纤维),作为未来版本Java的一部分,计划作为Project Loom的一部分开发。
目前,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() ;

注意:正如Michael所评论的那样,JVM管理的虚拟线程取决于主机操作系统管理的平台/内核线程。最终,即使在Loom下,执行也由操作系统调度。当阻塞的Java线程在CPU核心上空闲时,虚拟线程非常有用。如果主机计算机负载过重,Java线程可能会看到很少的执行时间,无论是否使用虚拟线程。

对于很少阻塞、真正CPU密集型的任务,虚拟线程不适合。例如,视频编码。这些任务应该继续使用传统的线程。

有关更多信息,请参阅Oracle的Ron Pressler或Loom团队的其他成员的启发性演示和采访。寻找最新的版本,因为Loom已经发展。


感谢您的回复。为什么虚拟线程不适用于真正需要大量 CPU 运算的任务? - alfer
1
Project Loom 不会改变 JVM 与本地操作系统线程在调度方面的交互方式。Loom 保留了几个本地线程作为虚拟线程的基础。如果操作系统决定其他任务更重要,它仍然可以决定不给这些本地线程分配 CPU 时间。OP 问操作系统是否可以根据哪些任务正在阻塞来安排某些任务的调度。答案是否定的,在 Loom 下仍然是如此。 - Michael
@alfer 虽然停车和切换虚拟线程的成本相当便宜,但并非免费。存在一些开销。如果任务需要在 CPU 上不间断地进行常量工作,则将线程交给另一个任务是没有意义的。应该允许 CPU 绑定任务在挂起之前执行一大块时间。平台/内核线程今天处理得很好。 - Basil Bourque
Java已经有了用于“重新调度”线程的工具,它被称为“fork / join框架”。 - akuzminykh

2
我本来认为被阻塞的线程可以暂时地重新安排运行其他提交的任务。

这就是其他线程的作用。如果你创建了X个线程,其中Y个被阻塞,你仍然有剩余的X-Y个线程去完成其他提交的任务。可以推测,选择数量X是根据实现和/或程序员的想法得出的并发任务数量最佳值。

你想知道为什么实现不忽略这个决策。答案在于选择合适的线程数量比让实现忽略该选择更有意义。


2

你的部分说法是正确的。

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

你不太正确的地方在于试图预期操作系统和Java结合的行为。看,线程的概念存在于操作系统和Java级别。但它们是两个不同的东西。因此,有Java线程和操作系统线程。Java线程是通过操作系统线程实现的。

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

操作系统可以回收操作系统线程并将其用于其他用途,也可以保持阻塞状态——这取决于情况。但即使操作系统线程被重用,Java线程仍将保持阻塞状态。在您的线程池场景中,仍然有9个Java线程被阻塞,只有一个Java线程在工作。


1
我认为最有帮助的心理模型,至少对我来说,是不要假设Java线程和操作系统线程之间存在任何特定的映射关系。每个Java线程可以由自己的本地线程支持,或者所有Java线程可以由同一个本地线程支持(因此根本没有真正的并行性),或者介于两者之间。这是JVM的实现细节。 - Michael
@Michael 这是我在这里经常看到的常见错误。 - dreamcrash
@Michael,哪些Java实现不会将每个Java线程映射到一个操作系统线程? - Basil Bourque
1
@BasilBourque 我从未说过有一个,虽然有数十个JVM存在,包括用于嵌入式系统等目的,我当然不会排除这种可能性。我说通常不应该以那种方式考虑它,就像通常不应该考虑字节码将映射到哪些本地指令一样。JVM提供了一个抽象层,并且大多数时候将其视为黑盒子是很方便的。这就是抽象的意义所在。 - Michael
@inquisitive,这9个Java线程将保持阻塞状态,但不会影响操作系统线程。如果我定义第二个线程池并向其提交任务,操作系统是否能够启动它们?这也是在使用并行流时定义自定义ForkJoinPool的原因,以避免过多的并行流使用公共ForkJoinPool并造成阻塞。 - alfer
显示剩余2条评论

1
如果一个线程因为等待锁而被阻塞,操作系统能否重新调度该线程执行其他任务直到锁可用?根据我的理解,它不能被重新调度,只能一直等待直到获得锁。但这似乎效率低下。
我认为你的想法完全错误。即使你的20个线程中有10个“空闲”,操作系统(或JVM)也不会消耗资源来管理这些空闲线程。虽然我们通常致力于让我们的线程尽可能地不被阻塞,以达到最高吞吐量,但是有很多时候我们编写的线程预计大部分时间都是空闲的。
如果我们向ExecutorService提交了100个任务,并且线程池中有10个线程:如果其中一个线程持有锁并且其他9个线程正在等待该锁,则只有持有锁的线程可以取得进展。我原本以为被阻塞的线程可以被临时重新调度以运行其他提交的任务。
不是重新调度线程,而是系统的CPU资源。如果您的10个线程中有9个被阻塞在线程池中,则应用程序中的其他线程(垃圾回收器)或其他进程可以在服务器上获得更多的CPU资源。现代操作系统非常擅长在任务之间进行切换,这种切换发生了很多次。这一切都很正常。
现在,如果您的问题确实是“如何提高应用程序的吞吐量”,那么您正在问正确的问题。首先,您应该确保您的锁尽可能细粒度化,以确保持有锁的线程所需的时间最少。如果阻塞发生得太频繁,则应考虑增加线程池中的线程数,以便更有可能同时运行某些作业。线程池中线程数量的优化非常应用程序特定。请参见我在此处发布的帖子获取更多详细信息:Concept behind putting wait(),notify() methods in Object class 另外一个你可能需要考虑的事情是将任务分成几个部分,以便将可以并发运行的部分与需要同步的部分分开。您可以有一个由10个线程执行并发工作的池,然后再有一个单独的线程执行需要锁定的操作。这就是为什么ExecutorCompletionService被编写的原因,以便下游的某些东西可以获取线程池的结果,并在它们完成时对其进行操作。如果您正确地执行此操作,则可以显着提高吞吐量,但这将使您的程序更加复杂,并且如果您正在处理大量的作业或大量的结果,则需要关注队列。
一个很好的重构示例是这样一种情况:你有一个处理作业,必须将结果写入数据库。如果在每个作业结束时,池中的每个线程都需要获取对数据库连接的锁定,那么就会出现很多争用锁的情况,从而降低并发性。相反,如果处理是在线程池中完成的,并且有一个单独的数据库更新线程,则可以关闭自动提交,并在提交之间按顺序对多个作业进行更新,这可以显着提高吞吐量。然而,使用由连接池管理的多个数据库连接可能是一个不错的解决方案。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接