Java虚拟机并发编程-中文完整版(高清、带书签)

所需积分/C币:50 2014-05-10 23:01:23 52.72MB PDF
收藏 收藏 5
举报

很好的一本并发编程书,自己做了完整的书签。方便阅读 第1章 并发的威力与风险 1 1.1 线程:程序的执行流程 1 1.2 并发的威力 1 1.3 并发的风险 4 1.4 小结 9 第2章 分工原则 11 2.1 从顺序到并发 11 2.2 在IO密集型应用程序中使用并发技术 13 2.3 并发方法对IO密集型应用程序的加速效果 19 2.4 在计算密集型应用程序中使用并发技术 20 2.5 并发方法对于计算密集型应用程序的加速效果 25 2.6 有效的并发策略 26 2.7 小结 27 第3章 设计方法 28 3.1 处理状态 28
第1章并发的威力与风靥3 这个秒表小程序启动之后会弹出一个简单的窗口界面,窗口界面上包含一个 Start按钮 和一个显示为“0”的 label控件。不幸的是,当我们点击 Start按纽之后,该按钮上的文字 并没有变为“Stop",同时 label控件上也没有显示当前已记录的秒数。更糟的是,这个程 序甚至没法响应我们的关闭请求(即点击窗口上的关闭按钮无效)。 在 Java gUI应用程序中,主事件分派线程主要负责通知U相关的事件,并将要执行 的操作委派给相应的业务逻辑。当我们点击 Start按钮之后,主事件分派线程的执行逻辑就 走到了事件处理函数 e action Performed中,而一旦执行到这里,主线程就会被执行计数操 作的 startCountingo函数所挟持。而当我们点击按钮或尝试关闭窗口时,虽然这些事件都会 被塞到事件队列中,但由于主线程一直被困在 start Counting(0函数里,所以程序永远无法响 应这些事件了 综上所述,我们所需要做的是启用一个额外的线程或一个tmer(在Jaa的实现中, timer也会在一个新线程中执行)并把计时任务分派给它,同时释放主事件分派线程 多线程除了可以降低应用程序响应时间,还有助于提升用户体验。除此之外,应用程 序也可以提前预估用户下一步可能执行的操作并提前完成其中关键的动作来提高响应速度, 如索引或缓存一些用户所需的数据等。 1.2.2使程序执行更快 请叫头检视一下你之前所写的那些应用程序。看看其中是否存在一些当前是以顺序方 式执行但其实可以并发执行的操作。如果有的话,你可以尝试将这些操作分派到独立线程 中,来加速这些应用程序的运行。 在实际L作当中其实有很多类型的应用程序可以通过并发的方式来进行提速,包括一 些服务程序、计算密集型应用程序和数据分析应用程序等。 1.服务程序 假设我们接到任务,要开发一个处理多个卖家发货清单的应用程序。该任务要求我们要 将某些规则和业务流程应用到每笔订单上,而订单之间的处理顺序不限。在这种场景下,如 果我们还是顺序地处理每笔订单,那么系统的处理速度青定上不去,同时在资源的利用方面 (指用多核处理器)也是一种浪费,因此我们的应用程序需能够并发地处理这些订单。 2.计算密集型应用程序 我曾经就职于一家化工公司,在那里我主要负责写一些计算冶炼厂各个部门提炼出来 的化学制剂成分的应用程序。这些程序的共同特点就是需要大量的计算,所以需要将问题 拆分为多个子任务、并发执行各个子任务并最终将各子任务的结果汇总合并,很明显,这 种方法可提高效率。也正是因为很多问题可以用分而治之的方法来解决,所以处理程序的 执行效率就完全取决于程序员的并发编程能力。 在本书中,函数一词对应Java语言中的 method,与通常译作“方法”是等价的,请广大读者注 意。—译者注 4第!章并发的威力与风险 3.数据分析程序 曾经有人找我帮忙开发一个个人金融理财方面的应用程序,主要功能是从一个Web服 务器获取当日股票价格和其他详情信息。该应用程序需要为客户展示其总资产净值以及每 只股票交易量的详细信息。所以对于一个有钱的客户来说,该应用程序可能需要追踪100 只不同的股票。在网络使用的高峰时段,从web上查询·只股票的信息可能需要好几秒。 而当程序下载好所有相关数据并开始处理的时候,客户可能已经等待好几分钟的时间了。 如果假设每个针对web服务的请求的网络时延为1~2秒并且系统可以为应用程序提供足 以创建数百个线程的瓷源和能力的话,我们就可以把从Web服务下载信息的请求分派到多 个线程,用这种方法可将客户的等待时间缩短至几秒。 1.23并发编程的优点 并发编程可以帮助应用程序提高响应速度、减少等待时间并增加吞吐量。我们可以充 分利用多核处理器的性能优势以及多任务并发的方法来提高程序运行效率和响应速度。但 正如我们在下一节将讨论的那样,在真正享受这些并发编程所带来的好处之前,我们还需 要克服重重困难,前方路上还有很多坑等着我们去填。 13并发的风险 看到这里,你可能会想“只把任务进行分解并分派到多个线程中执行,程序就能获 得更高的吞吐量”。但遗憾的是,绝大多数问题都无法被分解为彼此完全独立的几个子问 题。更普遍的情况是,我们可以独立地执行某些操作,但最终还是需要将这些操作所得到 的部分结果进行归并才能得到完整的结果。所以线程之间需要能够相互交换彼此的数据, 并且有时候某些线程还需要等待其他线程的结果出来之后才能继续运行。于是我们就需要 在线程之间进行协调,并由此引出同步和锁等一系列令人头痛的问题。 在开发并发应用程序的时候,我们通常会遇到3类问题:饥饿、死锁以及竞争条件。 其中前两个问题还算比较易于检测甚至避免,而竞争条件则是一个需要彻底根除的棘手 问题。 13.1饥饿和死锁 线程是很容易陷入饥饿状态的。例如,某应用程序正准备执行一个关键任务,但执行 该任务之前需要凭得到用户的确认,而此时正赶上用户在吃午饭。所以当用户吃得正香的 时候,可怜的应用程序就陷入了饥饿状态。当一个线程等待某个需要运行很长时间或永远 无法完成的事件发生时,该线程就会陷入饥饿。饥饿的情况可能出现在线程等待用户输入 时、等待某些外部事件发生时或等待其他线程释放某个锁时。当线程陷人这种等待状态时, 虽然其本身还是活着的,但却什么活也不能干。为了避免线程陷入饥俄,我们可以为其设 计一个等待超时的策略,让线程等待有限的时间。即如果所等待的用户输入迟迟不来、事 件一直不发生或线程在指定时间内一直没能拿到锁,则该线程将会跳出等待状态并在执行 第1章并发的威力与风险5 完超时处理逻辑后再继续运行。 死锁则是指两个或多个线程相互等待对方释放所占用的资源或执行某些动作。然而比 饥饿问题更麻烦的是,单凭设定等待超时是无法完全避免死锁的。因为在某些情况下,每 个线程因等待超时放弃了其占用的资源之后重做之前的任务,却发现最终又重蹈覆辙,详 情请参阅附录2中的“晢学家用餐问题”。虽然我们也有一些检测和避免死锁的方法,比如 使用 JConsole这样的工具或令所有线程按照某一特定顺序来获取资源等。但解决死锁问题 更好的方案莫过于避免显式加锁以及避免使用可变状态。在本书后面的章节中我们将详细 讨论如何使用这些方法。 13.2竞争条件 如果两个线程竞争使用相同的资源或数据,那么我们就将这种情况称为竞争条件(race condition)。竞争条件不仅仅会发生在两个线程同时更改相同数据的场景中,还可能发生在 一个线程正在修改某数据而另一个线程同时在读这个数据的时候。竞争条件可能会导致程 序行为不可控、执行逻辑异常并产生错误的结果。 竞争条件主要是由下面两大原因造成的:即时下流行的 Just-In- Time(JIT编译器的优化 以及Java内存模型。有关Java内存模型相关问题的论述以及该模型对并发的影响,请参阅 Brian goetze的著作《 Java Concurrency in Practice)Goe061。 下面让我们通过一个非常简单的例子来演示上面所提到的问题。在下面的代码中,主 线程首先创建了一个新线程,随即自己seep了2秒,最后再将变量done的值设为true 新线程启动之后,会在其内部循环中不停判断done变量的值,只要其为 false就一直循环。 下面让我们编译并运行下面的代码并观察其输出结果: introduction/Race Condition. java public class RaceCondition t private static boolean done public static void main( final string ln args) throws Interrupted Exception new Thread new Runnable()[ public void run《) inti=日 while(! done)( i++:) System. out. print ln("Done!"); ); system.out. printin(OS: " System getProperty("os. name") Thread. sLeep(26Bθ); done true: System.out. printLn("fLag done set to true") 6第1章并发的威力与鳳险 如果我们在 Windows7(32位版本)上使用 java Race Condition命令来运行上述程序, 则可以观察到类似下面的输出结果(每次运行后输出结果的顺序可能不同) oS: Windows 7 flag done set to true Done 而如果我们在Mac上尝试用同样的命令来运行上述程序,则会发现新线程里的循环永 远不会停止,即新线程没看到主线程对于done变量的修改。具体输出内容如下: 05: Mac os x 「 Lay done set tu true 等等,请不要把书扔到一边然后跑去发微博说¨ Windows太好用了,Mac真垃圾”。这 个问题其实远比我们从表面现象上所得到的结论复杂。 让我们再试一次,这回我们回到 Windows系统,执行命令java- server Race Condition (即设置程序在 Windows平台上以 server模式运行),然后再到Mac机上用这个命令java d32 Race condition(即设置程序在Mac上以 client模式运行)来运行上面的示例。 我们在 Windows平台上观察到的结果如下所示: OS: Windows 7 flag done set to true 然而这次Mac平台的输出则与之前不同: 0s: Mac o5 x Done flag done set to true 默认情况下,Java程序在32位 Windows平台上是以 client模式运行的,而在Mac平 台上则是以 server模式运行的。所以我们的程序在两个平台上的表现是一致的,即以 client 模式运行时可以终止而以 server模式运行时则无法终止 当程序以 server模式运行时,即使主线程将done变量的值设为true,第二个线程也无 法看到该变量值的变化。这种现象是由于 Java server jit编译器优化所导致的。但是让我们 权且先不要抱恕JITr编译器,因为它只是任劳任怨地进行代码优化以使程序跑得更快。 从上面的例子中我们所吸取到的教训是,一个烂程序可能在某些情况下工作正常而在 另外一些情况下则会失败。 1.3.3了解可见性:理解内存栅栏 上面那个示例存在的问題是,主线程对其字段done所做的变更对新创建出来的线程不 叮见。造成这种现象的首要原因是,JIT编译器可能对新线程代码里的 while循环进行了优 化,并因此导致新线程在线程上下文中无法看到变量done的变化。此外,新线程可能会只 从其寄存器或本地 cache中读取标记变量done的值,而不是每次都跑去速度更慢的内存里 进行操作。基于上述原因,新线程就无法看到主线程对其标记变量值的变更了,详情请参 阅下文“什么是内存栅栏”。 如果想要快速修复此问题,只需要将变量done标记为ν olatile就可以了。具体做法是将 第1章并发的威力与风险7 private static boolean done; 改为: private static volatile boolean done: 关键字 volatile的作用是告知JmT编译器不要对被标记变量执行任何可能影响其访问顺 序的优化。该关键字警告JT编译器,该变量可能会被某个线程更改,所以任何对该变量 的读写访问都需要忽略本地 cache并直接对内存进行操作。之前我将这个改动称为快速修 复,是因为如果我们将所有变量都标记为 yolatile的话,虽然可以完全规避此类问题,但却 会使每次变量访问都要跨越内存栅栏并最终导致程序性能下降。此外,在多个宇段被多个 线程并发访问的场景下,由于针对每个 volatile宇段的访问都是各自独立处理的,并且也无 法将这些访问统一协调成一次访问,所以 volatile关键字无法保证整体操作的原子性。该问 题所造成的后果是,线程很可能对某些宇段只能看到其中间结果,而对另一些变量则看到 的是最终的变更结果。 为了解决这个问题,我们可以屏蔽对变量的直接访问,并将所有访问都引导为通过同 步的getr和 setter函数来进行,具体代码如下所示: private static boolean done public static synchronized boolean getFLag()i return done; public static synchronized void setFLag( booLean flag) i done fLag: I 关键字 synchronized在这甲起到了至关重要的作用。由于 synchronized是为数不多的 几个可以令线程在进入和离开同步区块时都跨越内存栅栏的原语之一,所以如果多个线程 在相同的实例对象上进行同步并且先申请到对象锁的线程完成了对实例对象的操作,则后 面申请到对象锁的线程将肯定可以看到前面完成操作的线程所做的变更。再次提醒,要了 解该问题的详情请参阅下文“什么是内存栅栏”。 Joe问:什么是内存栅栏? 简单来说,内存棚栏( Memory barrier)就是从本地或工作内存到主存之间的拷贝动作。 仅当写操作线程先跨越内存栅栏(参见附录2中 Doug Lea所著《 The jSr-133 Cookbook for Compiler Writers》)而读线程后跨越内存栅栏的情况下,写操作线程所做的 变更才对其他线程可见。关键宇 synchronized和 volatile都强制规定了所有的变更必须仝 局可见,该特性有助于跨越内存边界动作的发生,无论是有意为之还是无心插柳。 在程序运行过程中,所有的变更会先在寄存器或本地 cache中完成,然后才会被拷贝 到主存以跨越内存栅栏。此种跨越序列或顺序称为 happens- before,详情请参阅附录2中的 “Jaa内存模型”以及B rlan goetz 的著作《 Java Concurrency in Practice)[Goel06。 写操作必须要 happens- before读操作,即写线程需要在所有读线程跨越内存栅栏之 前完成自己的跨越动作,其所做的变更才能对其他线程可见。 Java并发API中很多操作都隐含有跨越内存栅栏的含义: volatile、 synchronized、 Thread中的函数如star和 interrupt、 Executor Service中的函数以及像 CountDown Latch 这样的同步工具类等。 8第1章并发的威力与风险 1.3.4规避共享可变性 如果我们在本该使用 volatile戌 synchronized的地方忘了做同步的话,则可能会导致不 可预测的程序行为。但其实核心问题并非我们忘记做同步这件事本身,而是因为我们正在 处理共享可变性。 在过去的工作经历中,我们已经习惯于使用可变性来开发Jaa应用程序,即通过更 改其字段的方式来创建和变更一个对象状态。然而,像 Joshua bloch的《 Effective java》 [Blo08]这类伟大的著作则建议我们尝试将不可变性引入到并发编程当中,因为不可变性可 以帮助我们从根本上规避上面所提到的那些难题。 虽然很容易被误用以致程序逻辑出错,但可变性本身并非全无优点。共享是一件好事, 毕竟妈妈以前一直是这样教导我们的。虽然两件事分开看都还是不错的,但掺和在一起就 不是那么回事了。 假设我们在程序中定义了一个非 final(可变)的字段,每当一个线程更改了该字段的 值,我们都需要考虑是应该将变更后的值写回内存还是应该将其保留在寄存器 /cache中。 而每当读取该字段的时候,都需要关心所读到的内容究竞是最新的有效值还是 cache中旧的 过期结果。同时,还需要确保针对该变量的变更是原子的,即其他线程会不看到变更过程 的中间结果。此外,我们还需要为防止多个线程同时更改同一数据而操心。 对丁一个涉及可变性的应用程序而言,每个针对共享可变状态的独立访问我们都需要 验证其正确性。只要其中某次访问出问题,则整个程序就会出问题。由此可见,处理可变 性是一项艰巨的任务,因为只要并发处理的逻辑中存在一行有问题的代码,那么整个应用 程序就可能会挂掉。事实上,有相当数量的并发Java应用程序都存在这类问题,只是我们 不知道而已。 如果我们在程序中定义了一个指向某不可变实体的fnal(不可变)字段并让多个线程 同时访问该字段,则这种形式的共享就不会有任何隐患。任何线程都可以读取该实体的值, 并且这个值就是该实体保存在其 cache中的值的拷贝。由于值是不可变的,所以后续对该值 的访问只要从本地 cache中获取就行了,而我们则还可以坐享其成,从中获得更好的性能。 共享可变性是魔鬼,千万别碰它! 如果什么都不能改,那怎样才能让应用程序干活啊?话虽如此,但我们还是需要围绕 不可变性来设计应用程序。其中一种方案是对可变状态进行严密的封装并只共享不可变数 据。而另一种备选方案则是由纯函数式语言所提供的,即除了可以使用函数组合之外,将 程序中一切事物都设定为不可变的。值得注意的是,在方案二中,如果我们想要将一个不 可变状态转换成另一个不可变状态,则需要利用一系列的转换动作才能完成。除了上述两 种方法之外,我们还可以使用一个可以监控所有变化并在出现任何违规行为时对我们发出 警告的库来规避可变性所带来的问题。在后面的章节中,我们将先引入一些有问题的示例, e例如,像Jeva中的 String、 Integer以及Long这样的实体就是不可变的,而 String builder和 Arraylist 则是可变的。 章并发的威力与风险9 然后再用并发方法将问题解决,以这种形式为你逐一介绍这些技术。 14小结 无诊我们正在开发一个交互式客户端桌面应用程序还是一个后台高性能服务,无论是 从充分利用硬件性能增长的角度还是多核处理器逐渐普及的现状来考虑,并发对我们的编 程工作都将起到至关重要的作用。通过使用并发,我们就可以更有效地提升用户体验,提 高程序响应速度,并使程序在那些拥有强劲性能的机器上跑得更快。传统的基于共享可变 性处理的JvM并发编程模型存在很多冋题。在创建完线程之后,我们需要努力避免线程 陷人饥饿、死锁以及竞争条件当中,而这些都是极难定位并极易导致错误发生的冋题。通 过消除共享可变性,我们就可以从根本上解决上述这些问题,而不可变性的应用程序将会 使并发编程更简单、安全和有趣。在本书后面的章节,我们将会学习不可变性的具体实现 方式。 接下来,我们将讨论几种用于确定线程数量和分解任务的方法。

...展开详情
试读 127P Java虚拟机并发编程-中文完整版(高清、带书签)
立即下载 低至0.43元/次 身份认证VIP会员低至7折
    一个资源只可评论一次,评论内容不能少于5个字
    hengltyy 还是比较详细的
    2019-04-19
    回复
    名金 不错的资源
    2018-11-24
    回复
    深渊的雷坎特 很好的一本并发编程书.
    2018-05-31
    回复
    pynode 非常不错的资源,很详细,很好用。
    2018-03-23
    回复
    jeasonchen 好书,推荐!
    2017-12-06
    回复
    jjk_by 好书,要推荐!
    2017-10-09
    回复
    爱听江南雨 Good book, thanks for sharing
    2017-08-07
    回复
    ed35007 非常感谢了,找了好久终于找到了
    2017-03-25
    回复
    chainhou 还不错,之前看过实体的
    2016-09-05
    回复
    GatsbyNewton 书很好,很详细
    2016-07-10
    回复
    关注 私信 TA的资源
    上传资源赚积分,得勋章
    最新推荐
    Java虚拟机并发编程-中文完整版(高清、带书签) 50积分/C币 立即下载
    1/127
    Java虚拟机并发编程-中文完整版(高清、带书签)第1页
    Java虚拟机并发编程-中文完整版(高清、带书签)第2页
    Java虚拟机并发编程-中文完整版(高清、带书签)第3页
    Java虚拟机并发编程-中文完整版(高清、带书签)第4页
    Java虚拟机并发编程-中文完整版(高清、带书签)第5页
    Java虚拟机并发编程-中文完整版(高清、带书签)第6页
    Java虚拟机并发编程-中文完整版(高清、带书签)第7页
    Java虚拟机并发编程-中文完整版(高清、带书签)第8页
    Java虚拟机并发编程-中文完整版(高清、带书签)第9页
    Java虚拟机并发编程-中文完整版(高清、带书签)第10页
    Java虚拟机并发编程-中文完整版(高清、带书签)第11页
    Java虚拟机并发编程-中文完整版(高清、带书签)第12页
    Java虚拟机并发编程-中文完整版(高清、带书签)第13页
    Java虚拟机并发编程-中文完整版(高清、带书签)第14页
    Java虚拟机并发编程-中文完整版(高清、带书签)第15页
    Java虚拟机并发编程-中文完整版(高清、带书签)第16页
    Java虚拟机并发编程-中文完整版(高清、带书签)第17页
    Java虚拟机并发编程-中文完整版(高清、带书签)第18页
    Java虚拟机并发编程-中文完整版(高清、带书签)第19页
    Java虚拟机并发编程-中文完整版(高清、带书签)第20页

    试读已结束,剩余107页未读...

    50积分/C币 立即下载 >