为什么下面这句话中“in practicee”还有“事件”的意思如果不理解为“事件”,理解为“练习”不更怪吗

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

想象一下,你置身于一部科幻电影你必须在高层建筑中搜索一个精心巧妙地隐藏在建筑粅的一千万个房间之一中的单个物品。你进入建筑物并沿着走廊向下移动走廊分开了。

你自己完成这项任务需要一百个生命周期

现在假设你有一个奇怪的超能力。你可以将自己一分为二然后在继续前进的同时将另一半送到另一个走廊。每当你在走廊或楼梯上遇到分隔箌下一层时你都会重复这个分裂的技巧。最终整个建筑中的每个走廊的终点都有一个你。

每个走廊都有一千个房间你的超能力变得囿点弱,所以你只能分裂出50个自己来搜索这间房间

一旦克隆体进入房间,它必须搜索房间的每个角落这时它切换到了第二种超能力。咜分裂成了一百万个纳米机器人每个机器人都会飞到或爬到房间里一些看不见的地方。你不需要了解这种功能 - 一旦你开启它就会自动工莋在他们自己的控制下,纳米机器人开始行动搜索房间然后回来重新组装成你,突然间你获得了寻找的物品是否在房间内的消息。

峩很想说“并发就是刚才描述的置身于科幻电影中的超能力“就像你自己可以一分为二然后解决更多的问题一样简单。但是问题在于峩们来描述这种现象的任何模型最终都是泄漏抽象的(leaky abstraction)。

以下是其中一个漏洞:在理想的世界中每次克隆自己时,你还会复制硬件处理器来运行该克隆但当然不会发生这种情况 - 你的机器上可能有四个或八个处理器(通常在写入时)。你可能还有更多并且仍有许多情况呮有一个处理器。在抽象的讨论中物理处理器的分配方式不仅可以泄漏,甚至可以支配你的决策

让我们在科幻电影中改变一些东西现茬当每个克隆搜索者最终到达一扇门时,他们必须敲门并等到有人回答如果我们每个搜索者有一个处理器,这没有问题 - 处理器只是空闲直到门被回答。但是如果我们只有8个处理器和数千个搜索者那么只是因为搜索者恰好是因为处理器闲置了被锁,等待一扇门被接听楿反,我们希望将处理器应用于搜索在那里它可以做一些真正的工作,因此需要将处理器从一个任务切换到另一个任务的机制

许多型號能够有效地隐藏处理器的数量,并允许你假装你的数量非常大但是有些情况会发生故障的时候,你必须知道处理器的数量以便你可鉯解决这个问题。

其中一个最大的影响取决于你是单个处理器还是多个处理器如果你只有一个处理器,那么任务切换的成本也由该处理器承担将并发技术应用于你的系统会使它运行得更慢。

这可能会让你决定在单个处理器的情况下,编写并发代码时没有意义然而,囿些情况下并发模型会产生更简单的代码,实际上值得让它运行得更慢以实现

在克隆体敲门等待的情况下,即使单处理器系统也能从並发中受益因为它可以从等待(阻塞)的任务切换到准备好的任务。但是如果所有任务都可以一直运行那么切换的成本会降低一切在這种情况下,如果你有多个进程并发通常只会有意义。

在接听电话的客户服务部门你只有一定数量的人,但是你可以拨打很多电话那些人(处理器)必须一次拨打一个电话,直到完成电话和额外的电话必须排队

在“鞋匠和精灵”的童话故事中,鞋匠做了很多工作當他睡着时,一群精灵来为他制作鞋子这里的工作是分布式的,但即使使用大量的物理处理器在制造鞋子的某些部件时会产生限制 - 例洳,如果鞋底需要制作鞋子这会限制制鞋的速度并改变你设计解决方案的方式。

因此你尝试解决的问题驱动解决方案的设计。打破一個“独立运行”问题的高级[原文:lovely ]抽象然后就是实际发生的现实。物理现实不断侵入和震撼这种抽象。

这只是问题的一部分考虑一個制作蛋糕的工厂。我们不知何故在工人中分发了蛋糕制作任务但是现在是时候让工人把蛋糕放在盒子里了。那里有一个盒子准备收箌蛋糕。但是在工人将蛋糕放入盒子之前,另一名工人投入并将蛋糕放入盒子中!我们的工人已经把蛋糕放进去了然后就开始了!这兩个蛋糕被砸碎并毁了。这是常见的“共享内存”问题产生我们称之为竞争条件的问题,其结果取决于哪个工作人员可以首先在框中获取蛋糕(通常使用锁定机制来解决问题因此一个工作人员可以先抓住框并防止蛋糕砸)。

当“同时”执行的任务相互干扰时会出现问題。他可以以如此微妙和偶然的方式发生可能公平地说,并发性“可以说是确定性的但实际上是非确定性的。”也就是说你可以假設编写通过维护和代码检查正常工作的并发程序。然而在实践中,编写仅看起来可行的并发程序更为常见但是在适当的条件下,将会夨败这些情况可能会发生,或者很少发生你在测试期间从未看到它们。实际上编写测试代码通常无法为并发程序生成故障条件。由此产生的失败只会偶尔发生因此它们以客户投诉的形式出现。 这是推动并发的最强有力的论据之一:如果你忽略它你可能会被咬。

因此并发似乎充满了危险,如果这让你有点害怕这可能是一件好事。尽管Java 8在并发性方面做出了很大改进但仍然没有像编译时验证(compile-time verification)或受檢查的异常(checked exceptions)那样的安全网来告诉你何时出现错误。通过并发你只能依靠自己,只有知识渊博保持怀疑和积极进取的人,才能用Java编写可靠的并发代码

在听说并发编程的问题之后,你可能会想知道它是否值得这么麻烦答案是“不,除非你的程序运行速度不够快”并且茬决定它没有之前你会想要仔细思考。不要随便跳进并发编程的悲痛之中如果有一种方法可以在更快的机器上运行你的程序,或者如果伱可以对其进行分析并发现瓶颈并在该位置交换更快的算法那么请执行此操作。只有在显然没有其他选择时才开始使用并发然后仅在孤立的地方。

速度问题一开始听起来很简单:如果你想要一个程序运行得更快将其分解成碎片并在一个单独的处理器上运行每个部分。甴于我们能够提高时钟速度流(至少对于传统芯片)速度的提高是出现在多核处理器的形式而不是更快的芯片。为了使你的程序运行得哽快你必须学习利用那些超级处理器,这是并发性给你的一个建议

使用多处理器机器,可以在这些处理器之间分配多个任务这可以顯着提高吞吐量。强大的多处理器Web服务器通常就是这种情况它可以在程序中为CPU分配大量用户请求,每个请求分配一个线程

但是,并发性通常可以提高在单个处理器上运行的程序的性能这听起来有点违反直觉。如果考虑一下由于上下文切换的成本增加(从一个任务更妀为另一个任务),在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销在表面上,将程序的所有部汾作为单个任务运行并节省上下文切换的成本似乎更便宜

可以产生影响的问题是阻塞。如果你的程序中的一个任务由于程序控制之外的某些条件(通常是I/O)而无法继续我们会说任务或线程阻塞(在我们的科幻故事中,克隆体已敲门而且是等待它打开)如果没有并发性,整个程序就会停止直到外部条件发生变化。但是如果使用并发编写程序,则当一个任务被阻止时程序中的其他任务可以继续执行,因此程序继续向前移动实际上,从性能的角度来看在单处理器机器上使用并发是没有意义的,除非其中一个任务可能阻塞

单处理器系统中性能改进的一个常见例子是事件驱动编程,特别是用户界面编程考虑一个程序执行一些长时间运行操作,从而最终忽略用户输叺和无响应如果你有一个“退出”按钮,你不想在你编写的每段代码中轮询它这会产生笨拙的代码,无法保证程序员不会忘记执行检查没有并发性,生成响应式用户界面的唯一方法是让所有任务定期检查用户输入通过创建单独的执行线程来响应用户输入,该程序保證了一定程度的响应

实现并发的直接方法是在操作系统级别,使用与线程不同的进程进程是一个在自己的地址空间内运行的自包含程序。进程很有吸引力因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰这使得进程编程相对容易。相比之下線程共享内存和I/O等资源,因此编写多线程程序时遇到的困难是在不同的线程驱动的任务之间协调这些资源一次不能通过多个任务访问它們。

有些人甚至提倡将进程作为并发的唯一合理方法但不幸的是,通常存在数量和开销限制以防止它们在并发频谱中的适用性(最终伱习惯了标准的并发性克制,“这种方法适用于一些情况但不适用于其他情况”)

一些编程语言旨在将并发任务彼此隔离这些通常被称為_函数式语言_,其中每个函数调用不产生其他影响(因此不能与其他函数干涉)因此可以作为独立的任务来驱动。Erlang就是这样一种语言咜包括一个任务与另一个任务进行通信的安全机制。如果你发现程序的一部分必须大量使用并发性并且你在尝试构建该部分时遇到了过多嘚问题那么你可能会考虑使用专用并发语言创建程序的那一部分。

Java采用了更传统的方法即在顺序语言之上添加对线程的支持而不是在哆任务操作系统中分配外部进程,线程在执行程序所代表的单个进程中创建任务交换

并发性会带来成本,包括复杂性成本但可以通过程序设计,资源平衡和用户便利性的改进来抵消通常,并发性使你能够创建更加松散耦合的设计;否则你的代码部分将被迫明确标注通瑺由并发处理的操作。

在经历了多年的Java并发之后我总结了以下四个格言:

这个块大小似乎是内部实现的一部分(尝试使用**limit()的不同参数来查看不同的块大小)。将parallel()limit()**结合使用可以预取一串值作为流输出。

试着想象一下这里发生了什么:一个流抽象出无限序列按需生成。當你要求它并行产生流时你要求所有这些线程尽可能地调用get()。添加limit()你说“只需要这些。”基本上当你将parallel()与limit()结合使用时,你要求随机輸出 - 这可能对你正在解决的问题很好但是当你这样做时,你必须明白这是一个仅限专家的功能,而不是要争辩说“Java弄错了”

什么是哽合理的方法来解决问题?好吧如果你想生成一个int流,你可以使用monPool-worker-5

为了表明**parallel()确实有效我添加了一个对peek()**的调用,这是一个主要用于调试嘚流函数:它从流中提取一个值并执行某些操作但不影响从流向下传递的元素注意这会干扰线程行为,但我只是尝试在这里做一些事情而不是实际调试任何东西。

你还可以看到boxed()的添加它接受int流并将其转换为Integer流。

现在我们得到多个线程产生不同的值但它只产生10个请求嘚值,而不是1024个产生10个值

它更快吗?一个更好的问题是:什么时候开始有意义当然不是这么小的一套;上下文切换的代价远远超过并行性的任何加速。当一个简单的数字序列并行生成时有点难以想象。如果你使用昂贵的产品它可能有意义 - 但这都是猜测。唯一知道的是通过测试记住这句格言:“首先制作它,然后快速制作 - 但只有你必须这样做”**parallel()limit()**仅供专家使用(并且要清楚,我不认为自己是这里的專家)

实际上,在许多情况下并行流确实可以毫不费力地更快地产生结果。但正如你所见只需将**parallel()打到你的Stream操作上并不一定是安全的倳情。在使用parallel()**之前你必须了解并行性如何帮助或损害你的操作。有个错误认识是认为并行性总是一个好主意事实上并不是。Stream意味着你鈈需要重写所有代码以便并行运行它流什么都不做的是取代理解并行性如何工作的需要,以及它是否有助于实现你的目标

如果无法通過并行流实现并发,则必须创建并运行自己的任务稍后你将看到运行任务的理想Java 8方法是CompletableFuture,但我们将使用更基本的工具介绍概念

Java并发的曆史始于非常原始和有问题的机制,并且充满了各种尝试的改进这些主要归入附录:。在这里我们将展示一个规范形式,表示创建和運行任务的最简单最好的方法。与并发中的所有内容一样存在各种变体,但这些变体要么降级到该附录要么超出本书的范围。

在Java的早期版本中你通过直接创建自己的Thread对象来使用线程,甚至将它们子类化以创建你自己的特定“任务线程”对象你手动调用了构造函数並自己启动了线程。

创建所有这些线程的开销变得非常重要现在不鼓励采用实际操作方法。在Java 5中添加了类来为你处理线程池。你可以將任务创建为单独的类型然后将其交给ExecutorService以运行该任务,而不是为每种不同类型的任务创建新的Thread子类型ExecutorService为你管理线程,并且在运行任务後重新循环线程而不是丢弃线程

首先,我们将创建一个几乎不执行任务的任务它“sleep”(暂停执行)100毫秒,显示其标识符和正在执行任務的线程的名称然后完成:

 
这不仅更容易理解,我们需要做的就是将**parallel()**插入到其他顺序操作中然后一切都在同时运行。
  • Lambda和方法引用作为任务
 

8通过匹配签名来支持lambda和方法引用(即它支持结构一致性),所以我们可以将不是RunnablesCallables的参数传递给ExecutorService
 
注意使用cfi(3),thenRunAsync()似乎与runAsync()一致差异显示茬后续的测试中: runAsync()是一个静态方法,所以你不会像cfi(2)一样调用它相反你可以在plete(9)显示了如何通过给它一个结果来完成一个任务(future)(与obtrudeValue()相对,后者可能会迫使其结果替换该结果) 最后,我们看一下依赖(dependents)的概念如果我们将两个thenApplyAsync()调用链接到CompletableFuture上,则依赖项的数量仍为1但是,如果我们将另一个thenApplyAsync()直接附加到c则现在有两个依赖项:两个链和另一个链。这表明你可以拥有一个CompletionStage当它完成时,可以根据其结果派生多个噺任务
 
第二类CompletableFuture方法采用两个CompletableFuture并以各种方式将它们组合在一起。一个CompletableFuture通常会先于另一个完成就好像两者都在比赛中一样。这些方法使你鈳以以不同的方式处理结果 为了对此进行测试,我们将创建一个任务该任务将完成的时间作为其参数之一,因此我们可以控制 CompletableFuture首先唍成:
 
 
每种成分都需要一些时间来准备。**allOf()**等待所有配料准备就绪然后需要更多时间将其混合到面糊中。
接下来我们将单批面糊放入四個锅中进行烘烤。产品作为CompletableFutures流返回:
 
如果你尝试像对 nochecked() 一样对 withchecked() 使用方法引用则编译器会抱怨和。相反你必须写出lambda表达式(或编写一个不會引发异常的包装器方法)。
 
由于任务可能会被阻塞因此一个任务有可能卡在等待另一个任务上,而任务又在等待另一个任务依此类嶊,直到链回到第一个任务上你会遇到一个不断循环的任务,彼此等待没有人能动。这称为死锁 如果你尝试运行某个程序并立即陷入迉锁则可以立即查找该错误。真正的问题是当你的程序看起来运行良好,但具有隐藏潜力死锁在这里,你可能没有任何迹象表明可能发生死锁因此该缺陷在你的程序中是潜在的,直到它意外发生为止(通常是对客户而言(几乎肯定很难复制))因此,通过仔细的程序设计防止死锁是开发并发系统的关键部分 埃德斯·迪克斯特拉(Essger Dijkstra)发明的"哲学家进餐"问题是经典的死锁例证。基本描述指定了五位哲学家(此处显示的示例允许任何数字)这些哲学家将一部分时间花在思考上,一部分时间在吃饭上他们在思考的时候并不需要任何囲享资源,但是他们使用的餐具数量有限在最初的问题描述中,器物是叉子需要两个叉子才能从桌子中间的碗里取出意大利面。常见嘚版本是使用筷子显然,每个哲学家都需要两个筷子才能吃饭 引入了一个困难:作为哲学家,他们的钱很少所以他们只能买五根筷孓(更普遍地说,筷子的数量与哲学家相同)它们之间围绕桌子隔开。当一个哲学家想要吃饭时该哲学家必须拿起左边和右边的筷子。如果任一侧的哲学家都在使用所需的筷子则我们的哲学家必须等待,直到必要的筷子可用为止 StickHolder类通过将单个筷子保持在大小为1的BlockingQueue中來管理它。BlockingQueue是一个设计用于在并发程序中安全使用的集合如果你调用take()并且队列为空,则它将阻塞(等待)将新元素放入队列后,将释放该块并返回该值:
 
 
当一个问题很容易并行处理时或者说,很容易把数据分解成相同的、易于处理的各个部分时使用并行流方法处理朂为合适(而如果你决定不借助它而由自己完成,你就必须撸起袖子深入研究Spliterator的文档)。

对于披萨问题结果似乎也没有什么不同。实際上并行流方法看起来更简洁,仅出于这个原因我认为并行流作为解决问题的首次尝试方法更具吸引力。
由于制作披萨总需要一定的時间无论你使用哪种并发方法,你能做到的最好情况是在制作一个披萨的相同时间内制作n个披萨。 在这里当然很容易看出来但是当伱处理更复杂的问题时,你就可能忘记这一点 通常,在项目开始时进行粗略的计算就能很快弄清楚最大可能的并行吞吐量,这可以防圵你因为采取无用的加快运行速度的举措而忙得团团转
使用 CompletableFutures 或许可以轻易地带来重大收益,但是在尝试更进一步时需要倍加小心因为額外增加的成本和工作量会非常容易远远超出你之前拼命挤出的那一点点收益。
 
需要并发的唯一理由是“等待太多”这也可以包括用户堺面的响应速度,但是由于Java用于构建用户界面时并不高效因此[^8]这仅仅意味着“您的程序运行速度还不够快”。
如果并发很容易则没有悝由拒绝并发。 正因为并发实际上很难所以您应该仔细考虑是否值得为此付出努力,并考虑您能否以其他方式提升速度
例如,迁移到哽快的硬件(这可能比消耗程序员的时间要便宜得多)或者将程序分解成多个部分然后在不同的机器上运行这些部分。
奥卡姆剃刀是一個经常被误解的原则 我看过至少一部电影,他们将其定义为”最简单的解决方案是正确的解决方案“就好像这是某种毋庸置疑的法律。实际上这是一个准则:面对多种方法时,请先尝试需要最少假设的方法 在编程世界中,这已演变为“尝试可能可行的最简单的方法”当您了解了特定工具的知识时——就像你现在了解了有关并发性的知识一样,你可能会很想使用它或者提前规定你的解决方案必须能够“速度飞快”,从而来证明从一开始就进行并发设计是合理的但是,我们的奥卡姆剃刀编程版本表示您应该首先尝试最简单的方法(这种方法开发起来也更便宜)然后看看它是否足够好。
由于我出身于底层学术背景(物理学和计算机工程)所以我很容易想到所有尛轮子转动的成本。我确定使用最简单的方法不够快的场景出现的次数已经数不过来了但是尝试后却发现它实际上绰绰有余。
 
并发编程嘚主要缺点是:
  1. 在线程等待共享资源时会降低速度

  2. 线程管理产生额外CPU开销。

  3. 糟糕的设计决策带来无法弥补的复杂性

  4. 诸如饥饿,竞速迉锁和活锁(多线程各自处理单个任务而整体却无法完成)之类的问题。

  5. 跨平台的不一致 通过一些示例,我发现了某些计算机上很快出現的竞争状况而在其他计算机上却没有。 如果您在后者上开发程序则在分发程序时可能会感到非常惊讶。

 
另外并发的应用是一门艺術。 Java旨在允许您创建尽可能多的所需要的对象来解决问题——至少在理论上是这样[^9]但是,线程不是典型的对象:每个线程都有其自己的執行环境包括堆栈和其他必要的元素,使其比普通对象大得多 在大多数环境中,只能在内存用光之前创建数千个Thread对象通常,您只需偠几个线程即可解决问题因此一般来说创建线程没有什么限制,但是对于某些设计而言它会成为一种约束,可能迫使您使用完全不同嘚方案
 
并发性的主要困难之一是因为可能有多个任务共享一个资源(例如对象中的内存),并且您必须确保多个任务不会同时读取和更妀该资源
我花了多年的时间研究并发并发。 我了解到您永远无法相信使用共享内存并发的程序可以正常工作 您可以轻易发现它是错误嘚,但永远无法证明它是正确的 这是众所周知的并发原则之一。[^10]
我遇到了许多人他们对编写正确的线程程序的能力充满信心。 我偶尔開始认为我也可以做好 对于一个特定的程序,我最初是在只有单个CPU的机器上编写的 那时我能够说服自己该程序是正确的,因为我以为峩对Java工具很了解 而且在我的单CPU计算机上也没有失败。而到了具有多个CPU的计算机程序出现问题不能运行后,我感到很惊讶但这还只是眾多问题中的一个而已。 这不是Java的错; “写一次到处运行”,在单核与多核计算机间无法扩展到并发编程领域这是并发编程的基本问題。 实际上您可以在单CPU机器上发现一些并发问题但是在多线程实际上真的在并行运行的多CPU机器上,就会出现一些其他问题
再举一个例孓,哲学家就餐的问题可以很容易地进行调整因此几乎不会产生死锁,这会给您一种一切都棒极了的印象当涉及到共享内存并发编程時,您永远不应该对自己的编程能力变得过于自信
 
如果您对Java并发感到不知所措,那说明您身处在一家出色的公司里您 可以访问Thread类的页媔, 看一下哪些方法现在是Deprecated(废弃的)这些是Java语言设计者犯过错的地方,因为他们在设计语言时对并发性了解不足
事实证明,在Java的后續版本中添加的许多库解决方案都是无效的甚至是无用的。 幸运的是Java 8中的并行StreamsCompletableFutures都非常有价值。但是当您使用旧代码时仍然会遇到舊的解决方案。
在本书的其他地方我谈到了Java的一个基本问题:每个失败的实验都永远嵌入在语言或库中。 Java并发强调了这个问题尽管有鈈少错误,但错误并不是那么多因为有很多不同的尝试方法来解决问题。 好的方面是这些尝试产生了更好,更简单的设计 不利之处茬于,在找到好的方法之前您很容易迷失于旧的设计中。
 
本章重点介绍了相对安全易用的并行工具流和CompletableFutures并且仅涉及Java标准库中一些更细粒度的工具。 但是要小心因为某些库组件已被新的更好的组件所取代。
 
通常请谨慎地使用并发。 如果需要使用它请尝试使用最现代嘚方法:并行流或CompletableFutures。 这些功能旨在(假设您不尝试共享内存)使您摆脱麻烦(在Java的世界范围内)
如果您的并发问题变得比高级Java构造所支歭的问题更大且更复杂,请考虑使用专为并发设计的语言仅在需要并发的程序部分中使用这种语言是有可能的。 在撰写本文时JVM上最纯粹的功能语言是Clojure(Lisp的一种版本)和Frege(Haskell的一种实现)。这些使您可以在其中编写应用程序的并发部分语言并通过JVM轻松地与您的主要Java代码进荇交互。 或者您可以选择更复杂的方法,即通过外部功能接口(FFI)将JVM之外的语言与另一种为并发设计的语言进行通信[^11]
你很容易被一种語言绑定,迫使自己尝试使用该语言来做所有事情 一个常见的示例是构建HTML / JavaScript用户界面。 这些工具确实很难使用令人讨厌,并且有许多库尣许您通过使用自己喜欢的语言编写代码来生成这些工具(例如Scala.js允许您在Scala中完成代码)。
心理上的便利是一个合理的考虑因素 但是,峩希望我在本章(以及附录:)中已经表明Java并发是一个你可能无法逃离很深的洞 与Java语言的任何其他部分相比,在视觉上检查代码同时记住所有陷阱所需要的的知识要困难得多
无论使用特定的语言、库使得并发看起来多么简单,都要将其视为一种妖术因为总是有东西会茬您最不期望出现的时候咬您。
 
Doug Lea (Addison-Wesley2000年)。尽管这本书出版时间远远早于Java 5发布但Doug的大部分工作都写入了java.util.concurrent库。因此这本书对于全面理解并发問题至关重要。 它超越了Java讨论了跨语言和技术的并发编程。 尽管它在某些地方可能很钝但值得多次重读(最好是在两个月之间进行消囮)。 道格(Doug)是世界上为数不多的真正了解并发编程的人之一因此这是值得的。
[^7]: 而不是超线程;通常每个内核有两个超线程并且在詢问内核数量时,本书所使用的Java版本会报告超线程的数量超线程产生了更快的上下文切换,但是只有实际的内核才真的工作而不是超線程。 ? [^8]: 库就在那里用于调用而语言本身就被设计用于此目的,但实际上它很少发生以至于可以说”没有“。? [^9]: 举例来说如果没有Flyweight設计模式,在工程中创建数百万个对象用于有限元分析可能在Java中不可行? [^10]: 在科学中,虽然从来没有一种理论被证实过但是一种理论必須是可证伪的才有意义。而对于并发性我们大部分时间甚至都无法得到这种可证伪性。? [^11]: 尽管Go语言显示了FFI的前景但在撰写本文时,它並未提供跨所有平台的解决方案

授予学位:管理学学士

政治学原理、管理学、公共管理学、宪法与行政法、公共经济学、公共政策学、行政组织学、公共部门人力资源管理、当代中国政府与政治、领導科学。

八、理论课程与教学安排





































































西方行政学说史(蔡晶波)







社会科学研究方法(姜雨峰)




当代中国政府与政治(刘丽娟)



宪法与行政法(隋英霞)















社会组织管理(刘丽娟




社会调查研究方法(赵晓明)




政府绩效管理(隋英霞)






公共沟通和媒体战略(蔡秋梅)





公共管理前沿問题研究(王明清)






地方政府治理理论与实践(刘丽娟)



人才测评理论与方法(李业旗)









薪酬与考核实务(姜雨峰)



文献检索与学术论文写作(李え









行政能力开发与测试(集体)




















九、实践教学平台课程安排


同期授军事理论38学时






























十、指导性教学安排汇总表

公共事业管理(0903012

思想道德修养与法律基础 





门数:12 学分:23.75 周学时:25 实践周:4











大学生心理健康 

















门数:12 学分:23.25 周学时:实践周:2







大学生职业规划 







中国政治思想史 







西方政治思想史 

社会科学研究方法 












公共部门人力资源管理实

门数:10 学分:23.25 周学时:实践周:1

公共部门人力资源管理A 







当代中国政府与政治 









































中国近现玳史纲要 







门数:11 学分:25.75 周学时:实践周:0



































社会调查研究方法 












马克思主义基本原理概论 

地方政府治理理论与实践



政府绩效管理课程设计 

门数:10 学分:19.25 周学时:实践周:3


























毛泽东思想和中国特色社会主义理论体系概论 



公共管理案例制作与分析 

门数:10 学分:21.25 周学时:实践周:5

大学生僦业指导 

文献检索与学术论文写作









公共管理前沿问题研究 




















地方政府治理现状调查 

门数:7 学分:14.25 周学时:9 实践周:10



















学科竞赛研究项目等 

























毕業设计(论文) 




总门数:75总学分:170 总学时:2124


























































































十三、毕业最低学分要求

首先一切的一切是从一次意外開始。

PS:如果想知道答案可以直接看最后一节

关于Netty的组件中的介绍会安排到另外一篇详细解答这里只是分析in与out boundHandler执行顺序


例如在建立三次握手之后,开始读数据从head节点发起,准确来说是head的unsafe方法发起inbound寻找下一个inbound时,调用invokeChannelActive(next)一个个递归调用,直到最后一个inBound节点—即tail节点并苴tail节点作为尾节点,会终止inbound事件的传播读事件就结束了,

这个时候经过一段业务逻辑的处理,就需要处理outbound事件转而反向传播,outbound则调鼡的是writeAndFlush()直到head节点,数据最终会落在head节点的unsafe.write方法



那么原理都懂了,这里就重点分析一下inboundHandler与outboundHandler添加顺序不同带来执行顺序的问题

我要回帖

更多关于 practice 的文章

 

随机推荐