java

位置:IT落伍者 >> java >> 浏览文章

超线程多核心下Java多线程编程分析


发布日期:2022年12月20日
 
超线程多核心下Java多线程编程分析

Java环境下的多线程技术

构建线程化的应用程序往往会对程序带来重要的性能影响例如请考虑这样一个程序它从磁盘读取大量数据并且在把它们写到屏幕之前处理这些数据(例如一个DVD播放器)在一个传统的单线程程序(今天所使用的大多数客户端程序)上一次只有一个任务执行每一个这些活动分别作为一个序列的不同阶段发生只有在一块已定义大小的数据读取完成时才能进行数据处理因此能处理数据的程序逻辑直到磁盘读操作完成后才得到执行这将导致非常差的性能问题

在一个多线程程序中可以分配一个线程来读取数据让另一个线程来处理数据而让第三个线程把数据输送到图形卡上去这三个线程可以并行运行这样以来在磁盘读取数据的同时仍然可以处理数据从而提高了整体程序的性能许多大量的示例程序都可以被设计来同时做两件事情以进一步提高性能Java虚拟机(JVM)本身就是基于此原因广泛使用了多线程技术

本文将讨论创建多线程Java代码以及一些进行并行程序设计的最好练习另外还介绍了对开发者极为有用的一些工具和资源篇幅所限不可能全面论述这些问题所以我想只是重点提一下极重要的地方并提供给你相应的参考信息

线程化Java代码

所有的程序都至少使用一个线程在C/C++和Java中这是指用对main()的调用而启动的那个线程另外线程的创建需要若干步骤创建一个新线程然后指定给它某种工作一旦工作做完该线程将自动被JVM所杀死

Java提供两个方法来创建线程并且指定给它们工作第一种方法是子类化Java的Thread类(在javalang包中)然后用该线程的工作函数重载run()方法下面是这种方法的一个示例

public class SimpleThread extends Thread {

public SimpleThread(String str) {

super(str);

}

public void run() {

for (int i = ; i < ; i++) {

Systemoutprintln(i + + getName());

try {

sleep((long)(Mathrandom() * ));

} catch (InterruptedException e) {}

}

Systemoutprintln(DONE! + getName());

}

}

这个类子类化Thread并且提供它自己的run()方法上面代码中的函数运行一个循环来打印传送过来的字符串到屏幕上然后等待一个随机的时间数目在循环十次后该函数打印DONE!然后退出并由它杀死这个线程下面是创建线程的主函数

public class TwoThreadsDemo {

public static void main (String[] args) {

new SimpleThread(Do it!)start();

new SimpleThread(Definitely not!)start();

}

}

注意该代码极为简单函数开始给定一个名字(它是该线程将要打印输出的字符串)并且调用start()然后start()将调用run()方法程序的结果如下所示

Do it!

Definitely not!

Definitely not!

Definitely not!

Do it!

Do it!

Do it!

Definitely not!

Do it!

Definitely not!

Do it!

Definitely not!

Do it!

Do it!

Definitely not!

Do it!

Definitely not!

Definitely not!

Do it!

DONE! Do it!

Definitely not!

DONE! Definitely not!

正如你所看到的这两个线程的输出结果纠合到一起在一个单线程程序中所有的Do it!命令将一起打印后面跟着输出Definitely not!

这个程序的不同运行将产生不同的结果这种不确定性来源于两个方面在循环中有一个随机的暂停更为重要的是因为线程执行时间没法保证这是一个关键的原则JVM将根据它自己的时间表运行这些进程(虚拟机一般支持尽可能快地运行这些线程但是没法保证何时运行一个给定线程)对于每个线程可以使一个优先级与之相关联以确保关键线程被JVM处理在次要的线程之前

启动一个线程的第二种方法是使用一个实现Runnable接口的类这个接口也定义在javalang中这个Runnable接口指定一个run()方法然后该方法成为线程的主函数类似于前面的代码

现在Java程序的一般风格是支持继承的接口通过使用接口一个类在后面仍然能够继承(子类化)如果必要的话(例如如果该类要在后面作为一个applet使用的话就会发生这种情况)

线程的含义

在采用多线程技术增强性能的同时它也增加了程序内部运行的复杂性这种复杂性主要是由线程之间的交互引起的熟悉这些问题是很重要的因为随着越来越多的核心芯片加入到Intel处理器中要使用的线程数目也将相应地增长如果在创建多线程程序时不能很好地理解这些问题那么是调试时将很难发现错误因此让我们先看一下这些问题及其解决办法

等待另一个线程完成假定我们有一个整型数组要进行处理我们可以遍历这个数组每次一个整数并执行相应的操作更高效地我们可以建立多个线程这样以来让每个线程处理数组的一部分假定我们在开始下一步之前必须等待所有的线程结束为了暂时同步线程之间的活动这些线程使用了join()方法它使得一个线程等待另一个线程的完成加入的线程(线程B)等待被加入的线程(线程A)的完成在join()中的一个可选的超时值使得线程B可以继续处理其它工作如果线程A在给定的时间帧内还没有终止的话这个问题将触及到线程的核心复杂性等待线程的问题下面我们将讨论这个问题

在锁定对象上等待假定我们编写一个航空公司座位分配系统在开发这种大型的程序时为每个连接到该软件的用户分配一个线程是很经常的如一个线程对应一个机票销售员(在很大的系统中情况并非总是如此)如果有两个用户同时想分配同一个座位就会出现问题除非采取特殊的措施否则一个线程将分配该座位而另一个线程将会在做相同的事情两个用户都会认为他们在这趟航班上拥有一个分配的位子

为了避免两个线程同时修改一样的数据项我们让一个线程在修改数据前锁定数据项用这种方法当第二个线程开始作修改时它将等待到第一个线程释放锁为止当这种发生时线程将会看到座位已被分配而对于座位分配的请求就会失败两个线程竞争分配座位的问题也就是着名的竞争条件问题而当竞争发生时有可能导致系统的洩漏为此最好的办法就是锁定任何代码该代码存取一个可由多个线程共同存取的变量

在Java中存在好几种锁选择其中最为常用的是使用同步机制当一个方法的签名包含同步时在任何给定时间只有一个线程能够执行这个方法然后当该方法完成执行时对该方法的锁定即被解除例如

protected synchronized int reserveSeat ( Seat seat_number ){

if ( seat_numbergetReserved() == false ){

seat_numbersetReserved();

return ( );

}

else return ( );

}

就是一个方法在这种方法中每次只运行一个线程这种锁机制就打破了上面所描述的竞争条件

使用同步是处理线程间交互的几种方法中的一种JSE 中添加了若干方便的方法来锁定对象大多数这些方法可以在包ncurrentlocks中找到一旦你熟悉了Java线程就应该对它进行详细的研究

在锁机制解决了竞争条件的同时它们也带来了新的复杂性在这种情况下最困难的问题就是死锁假定线程A在等待线程B并且线程B在等待线程A那么这两个线程将永远被锁定这正是术语死锁的意义死锁问题可能很难判定并且必须相当小心以确保在线程之间没有这种依赖性

使用线程池

如前所提及在线程完成执行时它们将被JVM杀死而分配给它们的内存将被垃圾回收机制所回收不断地创建和毁灭线程所带来的麻烦是它浪费了时钟周期因为创建线程确实耗费额外的时间一个通用的且最好的实现是在程序运行的早期就分配一组线程(称为一个线程池)然后在这些线程可用时再使用它们通过使用这种方案在创建时分配给一个线程指定的功能就是呆在线程池中并且等待分配一项工作然后当分配的工作完成时该线程被返回到线程池

JSE 引入了ncurrent包它包括了一个预先构建的线程池框架这大大便利了上述方法的实现有关Java线程池的更多信息及一部教程请参见

在设计线程程序和线程池时自然出现关于应该创建多少线程的问题答案看你怎样计划使用这些线程如果你基于分离的任务来用线程划分工作那么线程的数目等于任务的数目例如一个字处理器可能使用一个线程用于显示(在几乎所有系统中的主程序线程负责更新用户接口)一个用于标记文档第三个用于拼写检查而第四个用于其它后台操作在这种情况中创建四个线程是理想的并且它们提供了编写该类软件的一个很自然的方法

然而如果程序象早些时候所讨论的那个一样使用多个线程来做类似的工作那么线程的最佳数目将是系统资源的反映特别是处理器上可执行管道的数目和处理器的数目的反映在采用英特尔处理器超线程技术(HT技术)的系统上当前在每个处理器核心上有两个执行管道最新的多核心处理器在每个芯片上有两个处理器核心英特尔指出将来的芯片有可能具有多个核心大部分是因为额外的核心会带来更高的性能而不会从根本上增加热量或电量的消耗因此管道数将会越来越多

照上面这些体系结构所作的算术建议在一个双核心Pentium 处理器系统上可以使用四条执行管道并因此可以使用四个线程将会提供理想的性能在一个双处理器英特尔Xeon?处理器的工作站上理想的线程数目是因为目前Xeon芯片提供HT技术但是没提供多核心模型你可以参考下面文档来了解这些新型处理器上的执行管道的数目(na/eng/)

小结

你当在平台上运行线程化的Java程序时你将可能想要监控在处理器上的加载过程与线程的执行最好的获得这些数据与管理JVM怎样处理并行处理的JVM之一是BEA的WebLogic JRockitJRockit还有其它一些由来自于BEA和Intel公司的工程师专门为Intel平台设计和优化的优点

不考虑你使用哪一种JVMIntel的VTune Performance Analyzer将会给你一个关于JVM怎样执行你的代码的很深入的视图这包括每个线程的性能瓶颈等另外Intel还提供了关于如何在Java环境下使用VTune Performance Analyzer的白皮书[PDF MB]

总之本文提供了线程在Java平台工作机理的分析由于Intel还将继续生产HT技术的处理器并且发行更多的多核心芯片所以想从这些多管道中得到性能效益的压力也会增加并且由于核心芯片数目的增加管道的数目也将相应地增加唯一的利用它们的优点的办法就是使用多线程技术如在本文中所讨论的并且Java多线程程序的优势也越来越明显

               

上一篇:Java设计模式之Adapter模式

下一篇:Java多线程发展简史