并发性
关键词:并发性,多面性,基本线程
使用顺序编程可以解决大部分编程问题。然而,对于某些问题,并行执行程序的几个部分变得方便甚至必不可少,以便这些部分看起来是同时执行的,或者如果有多个处理器可用,实际上是同时执行的。
正如您将看到的,当并行执行的任务开始相互干扰时,并发的真正问题就出现了。这可能以一种微妙而偶然的方式发生,以至于可以公平地说并发是“可以说是确定性的,但实际上是非确定性的”。也就是说,您可以提出一个论据来得出这样的结论:可以编写并发程序,通过小心和代码检查,可以正常工作。然而,在实践中,编写看似有效但在正确条件下会失败的并发程序要容易得多。这些情况可能永远不会真正发生,或者发生得如此罕见以至于您在测试期间从未见过它们。事实上,您可能无法编写会为并发程序生成失败条件的测试代码。由此产生的故障通常只会偶尔发生,因此它们会以客户投诉的形式出现。这是研究并发性的最有力论据之一:如果你忽略它,你很可能会被咬。
因此并发似乎充满了危险,如果这让你有点害怕,这可能是一件好事。尽管 Java SE5在并发性方面做出了重大改进,但仍然没有像编译时验证或检查异常这样的安全网来告诉您何时出错。有了并发,你就靠自己了,只有保持怀疑和积极进取,你才能用Java编写可靠的多线程代码。
人们有时认为并发性太先进了,无法包含在介绍该语言的书中。他们认为并发是一个可以独立处理的离散主题,它出现在日常编程中的少数情况(例如图形用户界面)可以用特殊的习语来处理。如果可以避免,为什么要引入如此复杂的话题?
唉,要是这样就好了。不幸的是,您无法选择线程何时出现在您的Java程序中。仅仅因为您从不自己启动线程并不意味着您将能够避免编写线程代码。例如,客户端系统是最常见的Java应用程序之一,而基本的客户端库类服务连接器本质上是多线程的——这是必不可少的,因为客户端服务器通常包含多个处理器,并发是利用这些处理器的理想方式。尽管服务连接器看起来很简单,但您必须了解并发问题才能正确使用服务连接器。图形用户界面编程也是如此,您将在图形用户界面一章中看到。尽管Swing和SWT库都具有线程安全机制,但如果不了解并发性,就很难知道如何正确使用这些机制。
Java是一种多线程语言,无论您是否意识到并发问题都存在。因此,有许多Java程序在使用中,要么只是偶然工作,要么大部分时间都在工作,并且由于未发现的并发缺陷而时不时地神秘地中断。有时这种破坏是良性的,但有时它意味着有价值数据的丢失,如果您至少没有意识到并发问题,您最终可能会认为问题出在其他地方而不是在您的软件中。如果将程序移至多处理器系统,这些问题也可能会暴露或放大。
并发的多面性
并发编程可能令人困惑的一个主要原因是使用并发解决的问题不止一个,实现并发的方法也不止一种,而且这两个问题之间没有清晰的映射(而且通常周围的界限都模糊了)。因此,您必须了解所有问题和特殊情况才能有效地使用并发。
速度问题一开始听起来很简单:如果您希望程序运行得更快,请将其分解为多个部分,然后在单独的处理器上运行每个部分。并发是多处理器编程的基本工具。现在,随着摩尔定律逐渐失效(至少对于传统芯片而言),速度的提高正以多核处理器的形式出现,而不是更快的芯片。为了让你的程序运行得更快,你必须学会利用那些额外的处理器,这是并发给你的一件事。
如果你有一台多处理器机器,多个任务可以分布在这些处理器上,这可以显着提高吞吐量。功能强大的多处理器客户端服务器通常会出现这种情况,它可以在为每个请求分配一个线程的程序中跨中央处理机分发大量用户请求。
但是,并发通常可以提高在单个处理器上运行的程序的性能。
这听起来有点违反直觉。如果您考虑一下,在单个处理器上运行的并发程序实际上应该比程序的所有部分顺序运行具有更多的开销,因为所谓的上下文切换(从一个任务切换到另一个任务)会增加成本.从表面上看,将程序的所有部分作为单个任务运行似乎更便宜,并且节省了上下文切换的成本。
可以产生影响的问题是阻塞。如果您的程序中的一项任务由于程序控制之外的某些条件(通常是I/O)而无法继续,我们称该任务或线程阻塞。如果没有并发,整个程序就会停止,直到外部条件发生变化。但是,如果程序是使用并发编写的,当一个任务被阻塞时,程序中的其他任务可以继续执行,因此程序继续前进。事实上,从性能的角度来看,在单处理器机器上使用并发是没有意义的,除非其中一个任务可能会阻塞。
单处理器系统中性能改进的一个非常常见的例子是事件驱动编程。事实上,使用并发最令人信服的原因之一是产生响应式用户界面。考虑一个执行一些长时间运行的操作并因此最终忽略用户输入并且没有响应的程序。如果你有一个“退出”按钮,你不想被迫在你写的每一段代码中轮询它。这会产生笨拙的代码,不能保证程序员不会忘记执行检查。如果没有并发,生成响应式用户界面的唯一方法是让所有任务定期检查用户输入。通过创建一个单独的执行线程来响应用户输入,即使该线程大部分时间都会被阻塞,程序也可以保证一定程度的响应能力。
程序需要继续执行其操作,同时还需要将控制权交还给用户界面,以便程序能够响应用户。但是传统方法不能继续执行其操作并同时将控制权返回给程序的其余部分。事实上,这听起来是不可能的,就好像中央处理机必须同时在两个地方一样,但这正是并发提供的假象(在多处理器系统的情况下,这不仅仅是一种假象)。
实现并发的一种非常直接的方法是在操作系统级别,使用进程。进程是在自己的地址空间内运行的自包含程序。多任务操作系统可以通过周期性地将中央处理机从一个进程切换到另一个进程来一次运行多个进程(程序),同时让每个进程看起来都在自己运行。进程非常有吸引力,因为操作系统通常将一个进程与另一个进程隔离,因此它们不会相互干扰,这使得使用进程进行编程相对容易。相比之下,Java中使用的并发系统共享内存和I/O等资源,所以编写多线程程序的根本难点在于协调不同线程驱动任务之间对这些资源的使用,以使它们一次不能被多个任务访问。
这是一个利用操作系统进程的简单示例。在写书时,我会定期对书的当前状态制作多个冗余备份副本。我将副本复制到本地目录中,一个在记忆棒上,一个在Zip磁盘上,一个在远程FTP站点上。为了使这个过程自动化,我写了一个小程序(用Python,但概念是一样的),它把书压缩到一个文件名中带有版本号的文件中,然后执行复制。最初,我按顺序执行所有副本,等待每个副本完成后再开始下一个副本。但后来我意识到每个复制操作所花费的时间取决于介质的I/O速度。由于我使用的是多任务操作系统,我可以将每个复制操作作为一个单独的进程启动,并让它们并行运行,从而加快整个程序的执行速度。当一个进程被阻塞时,另一个进程可以继续前进。
这是并发的理想示例。每个任务在自己的地址空间中作为一个进程执行,因此任务之间没有干扰的可能性。更重要的是,任务之间不需要相互通信,因为它们都是完全独立的。操作系统注意确保正确文件复制的所有细节。因此,没有风险,您可以免费获得更快的程序。
有些人甚至提倡将进程作为唯一合理的并发方法,但不幸的是,进程通常存在数量和开销限制,从而阻碍了它们在并发范围内的适用性。
一些编程语言旨在将并发任务彼此隔离。这些通常被称为函数式语言,其中每个函数调用都不会产生副作用(因此不会干扰其他函数),因此可以作为独立任务驱动。Erlang就是这样一种语言,它包含一个任务与另一个任务通信的安全机制。如果您发现您的程序的一部分必须大量使用并发,并且您在尝试构建该部分时遇到了过多的问题,您可能需要考虑使用专门的并发语言(如Erlang)创建程序的该部分。
Java采用了更传统的方法,即在顺序语言之上添加对线程的支持。线程不是在多任务操作系统中分叉外部进程,而是在由执行程序表示的单个进程内创建任务。这提供的一个优势是操作系统透明性,这是Java的一个重要设计目标。例如,Macintosh操作系统的前OSX版本(Java的第一个版本相当重要的目标)不支持多任务处理。除非在Java中添加了多线程,否则任何并发的Java程序都无法移植到Macintosh和类似平台上,从而打破了“一次编写/到处运行”的要求。
基本线程
并发编程允许您将程序划分为单独的、独立运行的任务。使用多线程,这些独立任务(也称为子任务)中的每一个都由执行线程驱动。线程是进程中的单个顺序控制流。因此,单个进程可以有多个并发执行的任务,但您在编程时就好像每个任务都有自己的CPU。一个底层机制为你分配了CPU 时间,但一般来说,你不需要考虑它。
定义任务
线程驱动任务,因此您需要一种描述该任务的方法。这是由Runnable接口提供的。要定义一个任务,只需实现Runnable并编写一个run( )方法来让任务按你的意愿行事。
任务的run( )方法通常有某种循环,该循环一直持续到不再需要该任务为止,因此您必须确定跳出此循环的条件(一种选择是简单地从run( )返回)。通常,run( )以无限循环的形式进行强制转换,这意味着,除非某些因素导致run( )终止,否则它将永远继续下去(在本章后面您将看到如何安全地终止任务)。
在run( )中调用静态方法Thread.yield( )是对线程调度程序(Java线程机制的一部分,将CPU从一个线程移动到下一个线程)的一个建议,它说:“我已经完成了我周期的重要部分,这将是一段时间切换到另一项任务的好时机。”它是完全可选的,但在这里使用它是因为它在这些示例中往往会产生更有趣的输出:您更有可能看到任务被换入和换出的证据。
任务的run( )不是由单独的线程驱动的;它只是在main( )中直接调用(实际上,这是使用一个线程:总是为main( )分配的线程)。
当一个类派生自Runnable时,它必须有一个run( )方法,但这并没有什么特别之处——它不会产生任何与生俱来的线程能力。要实现线程行为,您必须显式地将任务附加到线程。
使用执行器
Java SE5java.util.concurrent执行器通过为您管理线程对象来简化并发编程。执行器在客户端和任务执行之间提供了一层间接性;代替客户端直接执行任务,中间对象执行任务。执行器允许您管理异步任务的执行,而无需显式管理线程的生命周期。执行器是在Java SE5/6中启动任务的首选方法。
我们可以使用执行器而不是在MoreBasicThreads.java中显式创建线程对象。LiftOff对象知道如何运行特定任务;与命令设计模式一样,它公开了一个要执行的方法。ExecutorService(具有服务生命周期的执行器,例如关闭)知道如何构建适当的上下文来执行Runnable对象。在以下示例中,CachedThreadPool为每个任务创建一个线程。请注意,ExecutorService对象是使用静态 执行器方法创建的,该方法确定它将是哪种执行器:
很多时候,可以使用单个执行器来创建和管理系统中的所有任务。
对shutdown( )的调用会阻止新任务提交给该执行器。当前线程(在本例中是驱动main( )的线程)将继续运行在调用shutdown( )之前提交的所有任务。一旦执行器中的所有任务完成,程序将立即退出。
您可以轻松地将前面示例中的CachedThreadPool替换为不同类型的执行器。FixedT
剩余内容已隐藏,支付完成后下载完整资料
资料编号:[595982],资料为PDF文档或Word文档,PDF文档可免费转换为Word
课题毕业论文、文献综述、任务书、外文翻译、程序设计、图纸设计等资料可联系客服协助查找。