三、线程的含义
在采用多线程技术增强性能的同时,它也增加了程序内部运行的复杂性。这种复杂性主要是由线程之间的交互引起的。熟悉这些问题是很重要的,因为随着越来越多的核心芯片加入到Intel处理器中,要使用的线程数目也将相应地增长。如果在创建多线程程序时不能很好地理解这些问题,那么是调试时将很难发现错误。因此,让我们先看一下这些问题及其解决办法。
等待另一个线程完成:假定我们有一个整型数组要进行处理。我们可以遍历这个数组,每次一个整数并执行相应的操作。或,更高效地,我们可以建立多个线程,这样以来让每个线程处理数组的一部分。假定我们在开始下一步之前必须等待所有的线程结束。为了暂时同步线程之间的活动,这些线程使用了join()方法-它使得一个线程等待另一个线程的完成。加入的线程(线程B)等待被加入的线程(线程A)的完成。在join()中的一个可选的超时值使得线程B可以继续处理其它工作-如果线程A在给定的时间帧内还没有终止的话。这个问题将触及到线程的核心复杂性-等待线程的问题。下面我们将讨论这个问题。
在锁定对象上等待:假定我们编写一个航空公司座位分配系统。在开发这种大型的程序时,为每个连接到该软件的用户分配一个线程是很经常的,如一个线程对应一个机票销售员(在很大的系统中,情况并非总是如此)。如果有两个用户同时想分配同一个座位,就会出现问题。除非采取特殊的措施,否则一个线程将分配该座位而另一个线程将会在做相同的事情。两个用户都会认为他们在这趟航班上拥有一个分配的位子。
为了避免两个线程同时修改一样的数据项,我们让一个线程在修改数据前锁定数据项。用这种方法,当第二个线程开始作修改时,它将等待到第一个线程释放锁为止。当这种发生时,线程将会看到座位已被分配,而对于座位分配的请求就会失败。两个线程竞争分配座位的问题也就是著名的竞争条件问题,而当竞争发生时有可能导致系统的泄漏。为此,最好的办法就是锁定任何代码-该代码存取一个可由多个线程共同存取的变量。
在Java中存在好几种锁选择。其中最为常用的是使用同步机制。当一个方法的签名包含同步时,在任何给定时间只有一个线程能够执行这个方法。然后,当该方法完成执行时,对该方法的锁定即被解除。例如- protected synchronized int reserveSeat ( Seat seat_number ){
- if ( seat_number.getReserved() == false ){
- seat_number.setReserved();
- return ( 0 );
- }
- else return ( -1 );
- }
复制代码 就是一个方法-在这种方法中每次只运行一个线程。这种锁机制就打破了上面所描述的竞争条件。
使用同步是处理线程间交互的几种方法中的一种。J2SE 5.0中添加了若干方便的方法来锁定对象。大多数这些方法可以在包java.util.concurrent.locks中找到-一旦你熟悉了Java线程,就应该对它进行详细的研究。
在锁机制解决了竞争条件的同时,它们也带来了新的复杂性。在这种情况下,最困难的问题就是死锁。假定线程A在等待线程B,并且线程B在等待线程A,那么这两个线程将永远被锁定-这正是术语死锁的意义。死锁问题可能很难判定,并且必须相当小心以确保在线程之间没有这种依赖性。
四、使用线程池
如前所提及,在线程完成执行时,它们将被JVM杀死而分配给它们的内存将被垃圾回收机制所回收。不断地创建和毁灭线程所带来的麻烦是它浪费了时钟周期,因为创建线程确实耗费额外的时间。一个通用的且最好的实现是在程序运行的早期就分配一组线程(称为一个线程池),然后在这些线程可用时再使用它们。通过使用这种方案,在创建时分配给一个线程指定的功能就是呆在线程池中并且等待分配一项工作。然后,当分配的工作完成时,该线程被返回到线程池。
J2SE 5.0引入了java.util.concurrent包-它包括了一个预先构建的线程池框架-这大大便利了上述方法的实现。有关Java线程池的更多信息及一部教程,请参见http://java.sun.com/developer/JDCTechTips/2004/tt1116.html#2。
在设计线程程序和线程池时,自然出现关于应该创建多少线程的问题。答案看你怎样计划使用这些线程。如果你基于分离的任务来用线程划分工作,那么线程的数目等于任务的数目。例如,一个字处理器可能使用一个线程用于显示(在几乎所有系统中的主程序线程负责更新用户接口),一个用于标记文档,第三个用于拼写检查,而第四个用于其它后台操作。在这种情况中,创建四个线程是理想的并且它们提供了编写该类软件的一个很自然的方法。
然而,如果程序-象早些时候所讨论的那个一样-使用多个线程来做类似的工作,那么线程的最佳数目将是系统资源的反映,特别是处理器上可执行管道的数目和处理器的数目的反映。在采用英特尔处理器超线程技术(HT技术)的系统上,当前在每个处理器核心上有两个执行管道。最新的多核心处理器在每个芯片上有两个处理器核心。英特尔指出将来的芯片有可能具有多个核心,大部分是因为额外的核心会带来更高的性能而不会从根本上增加热量或电量的消耗。因此,管道数将会越来越多。
照上面这些体系结构所作的算术建议,在一个双核心Pentium 4处理器系统上,可以使用四条执行管道并因此可以使用四个线程将会提供理想的性能。在一个双处理器英特尔Xeon?处理器的工作站上,理想的线程数目是4,因为目前Xeon芯片提供HT技术但是没提供多核心模型。你可以参考下面文档来了解这些新型处理器上的执行管道的数目(http://www.intel.com/cd/ids/developer/asmo-na/eng/1Array6716.htm)。
五、小结
你当在平台上运行线程化的Java程序时,你将可能想要监控在处理器上的加载过程与线程的执行。最好的获得这些数据与管理JVM怎样处理并行处理的JVM之一是BEA的WebLogic JRockit。JRockit还有其它一些由来自于BEA和Intel公司的工程师专门为Intel平台设计和优化的优点。
不考虑你使用哪一种JVM,Intel的VTune Performance Analyzer将会给你一个关于JVM怎样执行你的代码的很深入的视图-这包括每个线程的性能瓶颈等。另外,Intel还提供了关于如何在Java环境下使用VTune Performance Analyzer的白皮书[PDF 2MB]。
总之,本文提供了线程在Java平台工作机理的分析。由于Intel还将继续生产HT技术的处理器并且发行更多的多核心芯片,所以想从这些多管道中得到性能效益的压力也会增加。并且,由于核心芯片数目的增加,管道的数目也将相应地增加。唯一的利用它们的优点的办法就是使用多线程技术,如在本文中所讨论的。并且Java多线程程序的优势也越来越明显。
|
|