Files
OnJava8/docs/book/24-Concurrent-Programming.md
2019-07-15 11:05:55 +08:00

10 KiB
Raw Blame History

[TOC]

第二十四章 并发编程

爱丽丝:“但是我不想进入疯狂的人群众”

猫咪“oh你无能为力我们都疯了我疯了你也疯了”

爱丽丝:“你怎么知道我疯了”。

猫咪:“你一定疯了,否则你不会来到这里”——爱丽丝梦游仙境 第6章。

到目前为止我们一直在编程就像文学中的意识流叙事设备一样首先发生一件事然后是下一件事。我们完全控制所有步骤及其发生的顺序。如果我们将值设置为5那么稍后会回来并发现它是47这将是非常令人惊讶的。

我们现在进入了一个奇怪的并发世界,在此这个结果并不令人惊讶。你信赖的一切都不再可靠。它可能有效,也可能没有。很可能它会在某些条件下有效,而不是在其他条件下,你必须知道和了解这些情况以确定哪些有效。

作为类比,你的正常生活是在牛顿力学中发生的。物体具有质量:它们会下降并移动它们的动量。电线具有阻力,过线可以直线传播。但是,如果你进入非常小、热、冷、或者大的世界(我们不能生存),这些事情会发生变化。我们无法判断某物体是粒子还是波,光是否受到重力影响,一些物质变为超导体。

而不是单一的意识流叙事,我们在同时多条故事线进行的间谍小说里。一个间谍在一个特殊的岩石下李璐下微缩胶片,当第二个间谍来取回包裹时,它可能已经被第三个间谍带走了。但是这部特别的小说并没有把事情搞得一团糟;你可以轻松地走到尽头,永远不会弄明白什么。

构建并发应用程序非常类似于游戏Jenga,每当你拉出一个块并将其放置在塔上时,一切都会崩溃。每个塔楼和每个应用程序都是独一无二的,有自己的作用。您从构建系统中学到的东西可能不适用于下一个系统。

本章是对并发性的一个非常基本的介绍。虽然我使用了最现代的Java 8工具来演示原理但这一章远非对该主题的全面处理。我的目标是为你提供足够的基础知识使你能够解决问题的复杂性和危险性从而安全的通过这些鲨鱼肆虐的困难水域。

对于更多凌乱,低级别的细节,请参阅附录:Appendix:Low-LevelConcurrency。要进一步深入这个领域你还必须阅读Brian Goetz等人的Java Concurrency in Practice。虽然在写作时这本书已有十多年的历史但它仍然包含你必须了解和理解的必需品。理想情况下本章和附录是该书的精心准备。另一个有价值的资源是Bill Venner的Inside the Java Virtual Machine它详细描述了JVM的最内部工作方式包括线程。

术语问题

在编程文献中并发、并行、多任务、多处理、多线程、分布式系统以及可能的其他使用了许多相互冲突的方式并且经常被混淆。Brian Goetz在2016年的演讲中指出了这一点From Concurrent to Parallel,他提出了一个合理的解释:

  • 并发是关于正确有效地控制对共享资源的访问。
  • 并行是使用额外的资源来更快地产生结果。

这些都是很好的定义但有几十年的混乱产生了反对解决问题的历史。一般来说当人们使用“并发”这个词时他们的意思是“一切变得混乱”事实上我可能会在很多地方自己陷入这种想法大多数书籍包括Brian Goetz的Java Concurrency in Practice都在标题中使用这个词。

并发通常意味着“不止一个任务正在执行中”,而并行性几乎总是意味着“不止一个任务同时执行。”你可以立即看到这些定义的区别:并行也有不止一个任务“正在进行”。区别在于细节,究竟是如何“执行”发生的。此外,重叠:为并行编写的程序有时可以在单个处理器上运行,而一些并发编程系统可以利用多个处理器。

这是另一种方法,在减速[原文slowdown]发生的地方写下定义:

并发

同时完成多个任务。在开始处理其他任务之前当前任务不需要完成。并发解决了阻塞发生的问题。当任务无法进一步执行直到外部环境发生变化时才会继续执行。最常见的例子是I/O其中任务必须等待一些input在这种情况下会被阻止。这个问题产生在I/O密集型。

并行

同时在多个地方完成多个任务。这解决了所谓的计算密集型问题,如果将程序分成多个部分并在不同的处理器上编辑不同的部分,程序可以运行得更快。

术语混淆的原因在上面的定义中显示其中核心是“在同一时间完成多个任务。”并行性通过多个处理器增加分布。更重要的是两者解决了不同类型的问题解决I/O密集型问题并行化可能对你没有任何好处因为问题不是整体速度而是阻塞。并且考虑到计算力限制问题并试图在单个处理器上使用并发来解决它可能会浪费时间。两种方法都试图在更短的时间内完成更多但它们实现加速的方式是不同的并且取决于问题所带来的约束。

这两个概念混合在一起的一个主要原因是包括Java在内的许多编程语言使用相同的机制线程来实现并发和并行。

我们甚至可以尝试添加细致的粒度去定义(但是,这不是标准化的术语):

  • 纯并发任务仍然在单个CPU上运行。纯并发系统产生的结果比顺序系统更快但如果有更多的处理器则运行速度不会更快
  • 并发-并行:使用并发技术,结果程序利用更多处理器并更快地生成结果
  • 并行-并发使用并行编程技术编写如果只有一个处理器结果程序仍然可以运行Java 8 Streams就是一个很好的例子)。
  • 纯并行:除非有多个处理器,否则不会运行。

在某些情况下,这可能是一个有用的分类法。

对并发性的语言和库支持似乎Leaky Abstraction是完美候选者。抽象的目标是“抽象出”那些对于手头想法不重要的东西,从不必要的细节中汲取灵感。如果抽象是漏洞,那些碎片和细节会不断重新声明自己是重要的,无论你试图隐藏它们多少

我开始怀疑是否真的有高度抽象。当编写这些类型的程序时你永远不会被底层系统和工具屏蔽甚至关于CPU缓存如何工作的细节。最后如果你非常小心你创作的东西在特定的情况下起作用但它在其他情况下不起作用。有时区别在于两台机器的配置方式或者程序的估计负载。这不是Java特有的-它是并发和并行编程的本质。

您可能会认为纯函数式语言没有这些限制。实际上,纯函数式语言解决了大量并发问题,所以如果你正在解决一个困难的并发问题,你可以考虑用纯函数语言编写这个部分。但最终,如果你编写一个使用队列的系统,例如,如果它没有正确调整并且输入速率要么没有被正确估计或被限制(并且限制意味着,在不同情况下不同的东西具有不同的影响),该队列将填满并阻塞或溢出。最后,您必须了解所有细节,任何问题都可能会破坏您的系统。这是一种非常不同的编程方式

并发的新定义

几十年来,我一直在努力解决各种形式的并发问题,其中一个最大的挑战一直是简单地定义它。在撰写本章的过程中,我终于有了这样的洞察力,我认为可以定义它:

并发性是一系列性能技术,专注于减少等待

这实际上是一个相当多的声明,所以我将其分解:

  • 这是一个集合:有许多不同的方法来解决这个问题。这是使定义并发性如此具有挑战性的问题之一,因为技术差别很大
  • 这些是性能技术就是这样。并发的关键点在于让您的程序运行得更快。在Java中并发是非常棘手和困难的所以绝对不要使用它除非你有一个重大的性能问题 - 即使这样,使用最简单的方法产生你需要的性能,因为并发很快变得无法管理。
  • “减少等待”部分很重要而且微妙。无论例如你运行多少个处理器你只能在等待某个地方时产生结果。如果您发起I/O请求并立即获得结果没有延迟因此无需改进。如果您在多个处理器上运行多个任务并且每个处理器都以满容量运行并且任何其他任务都没有等待那么尝试提高吞吐量是没有意义的。并发的唯一形式是如果程序的某些部分被迫等待。等待可以以多种形式出现 - 这解释了为什么存在如此不同的并发方法。

值得强调的是,这个定义的有效性取决于等待这个词。如果没有什么可以等待,那就没有机会了。如果有什么东西在等待,那么就会有很多方法可以加快速度,这取决于多种因素,包括系统运行的配置,你要解决的问题类型以及其他许多问题。

并发的超能力

针对速度

四句格言

残酷的真相

本章其余部分

并行流

创建和运行任务

终止耗时任务

CompletableFuture类

死锁

构造函数非线程安全

复杂性和代价

本章小结