并发外文翻译资料

 2022-07-26 15:36:07

英语原文共 359 页,剩余内容已隐藏,支付完成后下载完整资料


第10章

并发

线程(Thread)机制允许同时进行多个活动。并发程序设计比单线程程序设计要困难得多,因为有更多的东西可能出错,也很难以重现失败。但是你无法避免并发,因为我们所做的大部分事情都需要并发,而且并发也是能否从多核的处理器中获得好的性能的一个条件,这些现在都是很平常的事了。本章阐述的建议可以帮助你编写清晰、正确、文档组织良好的并发程序。

  1. :同步访问共享的可变数据

关键字synchronzied可以保证在同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。许多程序员把同步的概念仅仅理解为一种互斥的方式,即,当一个对象被一个线程修改的时候,可以组织另一个线程观察到对象内部不一致的状态。按照这种观点,对象被创建的时候处于一致的状态,当有方法访问它的时候,它就被锁定了。这些方法观察到对象的状态,并且可能会引发状态转变(state transition),即把对象从一种一致的状态转换到另一种一致的状态。正确地使用同步可以保证没有任何方法会看到对象处于不一致的状态中。

这种观点是正确的,但是它并没有说明同步的全部意义。如果没有同步,一个线程的变化就不能被其他线程看到。同步不仅可以组织一个线程看到对象处于不一致的状态之中,他还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前的所有的修改效果。

Java语言规范保证读或写一个原子的(atomic),除非这个变量的类型为long或者都double。换句话说,读取一个非long或者double类型的变量,可以保证返回的值是某个线程保存在该变量中的,即使多个线程在没有同步的情况下并发地修改这个变量也是如此。

你可能听说过,为了提高性能,在读或写数据的时候,应该避免使用同步。这个建议是非常危险而错误的。虽然语言规范保证了线程在读取原子数据的时候,不会看到任意的数值,但是它并不保证一个线程写入的值对于另一个线程将是可见的。为了在线程之间进行可靠地通信,也为了互斥访问,同步是必要的。这归因于java语言规范中内存模型(memory model),它规定了一个线程所做的变化何时以及如何变成对其他线程可见。

如果对共享的可变数据的访问不能同步,其后果将非常可怕,及时这个变量是原子可读写的。考虑下面这个阻止一个线程妨碍另一个线程的任务。Java的类库中提供了Tfread.stop方法,但是这个方法在很久以前就不提倡使用,因为它本质上是不安全的,使用它会导致数据遭到破坏。不要使用Thread.stop。要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询(poll)一个boolean域,这个域一开始为false,但是可以通过第二个线程设置为true,以表示第一个线程将终止自己。由于boolean域的读和写操作都是原子的,程序员在访问这个域的时候不在使用同步:

Public class StopThred{

Private static boolean stopRequested;

Public static void main(String[] args)throws InterruotedException{

Thread backgroundThread = new Thread(new Runable(){

Public void run(){

Int i = 0;

While(!stopRequested)

I ;

}

});

backgroudThread.start();

Timeunit.SECOND.sleep(1);

stopRequested

}

}

你可能期待这个程序运行大约一秒钟左右,之后主线程stopRequested设置为true,致使后台线程的循环终止。但是在我的机器上,这个程序永远不会终止:因为后台线程永远在循环。

避免本条目中所讨论到的问题的最佳办法是不共享可变的数据。要么共享不可变的数据,要么压根不共享。换句话说,将可变数据限制在单个线程中。如果采用这一策略,对它建立文档就很重要,以便它可以随着程序的发展而得到维护。深刻地理解正在使用的框架和类库也很重要,因为它们引入了人们所不知道的线程。

让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象引用的动作。然后其他线程没有进一步的同步也是可以读取对象,只是它没有再被修改。这种对象被称作事实上不可变(effectively immutable)。将这种对象引用从一个线程传递到其他的线程被称作为安全发布。安全发布对象引用有许多方法:可以将他保存在静态域中,作为类初始化的一部分;可以将它保存在volatile域,final域或者通过正规访问的域中;或者可以将它放大到并发的集合中。

简而言之,当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所作的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败和安全性失败。这样的失败是最难以调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同VM上可能根本不同。如果只需要线程之间的相互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。

237页

在同步区域之外被调用的外来方法被称作“开放调用(open call)”。除了可以避免死锁之外,开放调用看还可以极大地增加并发性。外来方法的运行时间可能会任意长。如果在同步区域内调用外来方法,其他线程对受保护资源的访问就会遭到不必要的拒绝。

通常吗你应该再同步区域内做尽可能少的工作。获得锁,检索共享数据,根据需要转换数据,然后放掉锁。如果你必须要执行某个很耗时的动作,则应该设法把这个动作移到同步区域的外面,而不违背第66条中的知道方针。

本条目的第一部分是关于正确性的。接下来,我们简单地讨论一下性能。虽然自从java平台早期以来,同步的成本已经下降了,但更重要的是,永远不要过度同步。在这个多核的时代,多度同步的实际成本并不是指获取锁所花费的CPU时间;而是指失去了并行的机会,以及因为要确保每个核都有一个一致的内存试图而导致的延迟。过度同步的另一项潜在开销在于,它会限制VM优化代码执行的能力。

如果一个可变的类要并发使用,应该是这个类变成线程安全的,通过内部同步,你还可以获得明显比外部锁定整个对象更高的并发性。否则,就不要在内部同步。让客户在必要的时候从外部同步。在java平台出现的早期,许多类都违背了这些指导方针。例如,StringBuffer实例几乎总是被用于单个线程之中,而它们执行的却是同步的。为此,StringBuffer基本上都由StringBuffer代替,他在java 1.5发行版中是个非同步的StringBuffer。当你不确定的时候,就不要同步你的类,而是应该建立文档,注明它不是线程安全的。

如果你在内部同步了类,就可以使用不同的方法来实现该高并发性,例如分拆锁,分离锁和非阻塞来并发控制。这些方法都超出了本书的讨论范围,但有其他著作对此进行了阐述。

如果方法修改了静态域,那么你也必须同步对这个域的访问,即使它往往只用于单个线程。客户要在这种方法上执行外部同步是不可能的,因为不可能保证其他不相关的客户也会执行外部同步。

简而言之,为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。更为一般地讲,要尽量限制同步区域内部的工作量,当你在设计一个不可变类的时候,要考虑一下它们是否应该自己完成同步操作。在现在这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该讲这个决定清楚地写到文档中。

239页

如果想让不止一个线程来处理来自这个队列的请求,只要调用一个不同的静态工厂,这个工厂创建了一种不同的executor service,称作线程池thread pool。你可以用固定或者可变数目的线程创建一个线程池。Java.util.concurrent.Executors类包含了静态工厂,能为你提供所需的大多数executor。然而,如果你想来点特别的,可以直接使用ThreadPoolExecutor类。这个类允许你控制线程池操作的几乎每个方面。

为特殊的应用程序选择executor service是很有技巧的。如果编写的是小程序,或者是轻载的服务器,使用Executors.newCachedThreadPool通常是个不错的选择,因为他不需要配置,并且一般情况下能够正确的完成工作。但是对于大负载的服务器来说,缓存的线程池就不是很好的选择了!在缓存的线程池中,被提交的任务没有排成队刘,而是直接交给线程执行。如果没有线程可用,就创建一个新的线程。如果服务器负载得太重,以致它所有的CPU都完全被占用了,当有更多的任务时,就会创建更多的线程,这样只会视情况变得更糟。因此,在大负载的产品服务器中,最好使用Executors.newFixedThreadPool,它为你提供了一个包含固定线程数目的线程池,或者为了最大限度地控制它,就直接使用ThreadPoolExecutor类。

你不仅应该尽量不要编写自己的工作队列,而且还应该尽量不直接使用线程。现在关键地抽象不再是theadl,它以前可是既充当工作单元,又是执行机制。现在工作单元和执行机制是分开的。现在关键的抽象是工作单元,称作任务。任务有两种:Runnable及其近亲Callable。执行任务的通用机制是executor service。如果你从任务的角度来看问题,并让一个executor service替你执行任务,在选择适当的执行策略方面就获得了极大地灵活性。从本质上讲,Executor Framework所做的工作是执行,犹如Collections Framework所做的工作是聚集一样。

Executor Framework也有一个可以代替java.util.Timer的东西,即ScheduledThreadPoolexecutor。虽然timer使用起来更加容易,但是被调度的线程池executor更加灵活。Timer只用一个线程来执行任务,这在面对长期运行的任务时,会影响到定时的准确性。如果timer唯一的线程抛出未被捕获的异常,timer就会停止执行。被调度的线程池executor支持多个线程,并且优雅地从抛出未受检异常的任务中恢复。

第11章

序列化

本章关注对象序列化(object serialization)API,它提供了一个框架,用来将对象编码成字节流,并且从字节流编码中重新构建对象、“将一个对象编码成一个字节流”,称作将该对象序列化;相反的处理过程被称作反序列化。一旦对象被序列化后,他的编码就可以从一台正在运行的虚拟机被传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用。序列化技术为远程通信提供了标准的线路级对象表示法,也为javaBeans组件就够提供了标准的持久化数据格式。本章中有一项值得特别提及的特性,就是序列化代理模式,他可以帮助你避免对象序列化的许多缺陷。

要想使一个累的实力可被序列化,非常简单,只要在它的声明中加入“implements Serializable”字样即可。正因为太容易了,所以普遍存在这样一种误解,认为程序员毫不费力就可以实现序列化。实际的情形要复杂得多。虽然使一个类可被序列化的直接开销非常低,甚至可以忽略不计,但是为了序列化而付出的长期开销往往是实实在在的。

实现serializable接口而付出的最大代价是,一旦一个类被发布,就大大降低了“改变这个类的实现”的灵活性。如果一个类实现了Serializable接口,他的字节流编码就变成了它的导出API的一部分,一旦这个类被广泛使用,往往必须永远支持这种序列化形式,就好像你必须支持导出的API的所有其他部分一样。如果你不努力设计一种自定义的序列化形式,而仅仅接受了默认的序列化形式,这种序列化形式将永远地束缚在该类最初的内部表示法上。换句话说,如果你接受了默认的序列化形式,这个类中私有的和包级私有的实力域将都变成导出的API的一部分,这不符合“最低限度的访问域”的实现准则,从而它就失去了作为信息隐藏工具的有效性。

如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。客户程序企图用这个类的旧版本来序列化一个类,然后用新版本进行反序列化,结果导致程序失败。在改变内部表示法的是同时仍然维持原来的序列化形式,这也是可能的,但是做起来比较困难,并且会在源代码中留下一个明显的隐患。因此,你应该仔细地设计一宗高质量的序列化形式,并且在很长时间内都愿意使用这种形式。这样做将会增加开发的初始成本,但这是值得的。设计良好的序列化形式也许会给类的演变带来限制,但是设计不好的序列化形式则可能会使类根本无法演变。

序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符有关,通常它也被称为序列版本UID。每个可序列化的类都有一个唯一标示号与它相关联。如果你没有在一个名为serialVersionUID的私有静态final的long域中显示地制定该标识号,系统就会自动地根据这个类来调用一个复杂得运算过程,从而在运行时产生该标示号。这个自动产生的值会受到类名称、它所实现的接口的名称,以及所有共有的和受保护的成员的名称所影响。如果你通过任何方式改变了这些信息,比如,增加了一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化。因为,如果你没有声明一个显示地序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。

实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。通常情况下,对象是利用

全文共6582字,剩余内容已隐藏,支付完成后下载完整资料


资料编号:[144637],资料为PDF文档或Word文档,PDF文档可免费转换为Word

原文和译文剩余内容已隐藏,您需要先支付 30元 才能查看原文和译文全部内容!立即支付

以上是毕业论文外文翻译,课题毕业论文、任务书、文献综述、开题报告、程序设计、图纸设计等资料可联系客服协助查找。