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

一、Java环境下的多线程技术

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

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

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

二、线程化Java代码

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

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

 

       
       public class SimpleThread extends Thread {
 public SimpleThread(String str) {
  super(str);
 }
 public void run() {
  for (int i = 0; i < 10; i++) {
   System.out.println(i + " " + getName());
   try {
    sleep((long)(Math.random() * 1000));
   } catch (InterruptedException e) {}
  }
  System.out.println("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()方法。程序的结果如下所示:

0 Do it!

0 Definitely not!

1 Definitely not!

2 Definitely not!

1 Do it!

2 Do it!

3 Do it!

3 Definitely not!

4 Do it!

4 Definitely not!

5 Do it!

5 Definitely not!

6 Do it!

7 Do it!

6 Definitely not!

8 Do it!

7 Definitely not!

8 Definitely not!

9 Do it!

DONE! Do it!

9 Definitely not!

DONE! Definitely not!

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

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

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

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

三、线程的含义

在采用多线程技术增强性能的同时,它也增加了程序内部运行的复杂性。这种复杂性主要是由线程之间的交互引起的。熟悉这些问题是很重要的,因为随着越来越多的核心芯片加入到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/196716.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多线程程序的优势也越来越明显。

 
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java2核心技术第I卷.基础知识 目录: 译者序 前言 第1章Java程序设计概述 1.1 Java程序设计平台 1.2 Java“白皮书”的关键术语 1.2.1简单性 1.2.2面向对象 1.2. 3分布式 1. 2.4健壮性 1. 2.5安仝性 1. 2.6体系结构中立 1.2.7可移植性 1.2.8解释型 1.2.9高性能 1.2.10多线程 1.2.11动态性 1. 3 Java与Internet 1. 4 Java发展简史 1.5关于Java的常见误解 第2章Java程序设计环境 2.1安装Java开发工具箱 2.1.1下载JDK 2.1.2设置执行路径 2.1.3安装库源代码和文档 2.1.4安装本书中的示例 2.1.5导航Java目录 2.2选择开发环境 2.3使用命令行工具 2.4使用集成开发环境 2.5使用文本编辑器编译并运行程序 2.6运行图形化应用程序 2.7建立并运行applet 第3章Java基本的程序设计结构 3.1一个简单的Java应用程序 3.2注释 3.3数据类型 3.3.1整型 3.3.2浮点型 3.3.3 char类型 3.3.4 boolean类型 3.4变量 3.4.1初始化变量 3.4.2常量 3.5运算符 3.5.1自增运算符与自减运算符 3.5.2关系运算符与boolean运算符 3.5.3位运算符 3.5.4数学函数与常量 3.5.5数值类型之间的转换 3.5.6强制类型转换 3.5.7括号与运算符级别 3.5.8枚举类型 3.6字符串 3.6.1代码点与代码单元 3.6.2子串 3.6.3字符串编辑 3.6.4拼接 3.6.5检测字符串是否相等 3.6.6阅读联机API文档 3.7输入输出 3.7.1读取输入 3.7.2格式化输出 3.8控制流程 3.8.1块作用域 3.8.2条件语句 3.8.3循环 3.8.4确定循环 3.8.5多重选择――switch语句 3.8.6中断控制流程语句 3.9大数值 3.10数组 3.10.1 “for each”循环 3.10.2数组初始化器以及匿名数组 3.10.3数组拷贝 3.10.4命令行参数 3.10.5数组排序 3.10.6多维数组 3.10.7不规则数组 第4章对象与类 4.1面向对象程序设计概述 4.1.1 OOP词汇表 4.1.2对象 4.1.3类之间的关系 4.1.4 00P与传统的过程化程序设计技术对比 4.2使用现有类 4.2.1对象与对象变量 4.2.2 Java库中的GregorianCalendar类 4.2.3更改器方法与访问器方法 4.3用户自定义类 4.3.1一个Employee类 4.3.2多个源文件的使用 4.3.3解析Employee类 4.3.4从构造器开始 5.2.2相等测试与继承 5.2.3 hashCode方法 5.2.4 toString方法 5.3泛型数组列表 5.3.1访问数组列表元素 5.3.2类型化与原始数组列表的兼容性 5.4对象包装器与自动打包 5.5反射 5.5.1 Class类 5.5.2使用反射分析类的能力 5.5.3在运行时使用反射分析对象 5.5.4使用反射编写通用的数组代码 5.5.5方法指针 5.6枚举类 5.7继承设计技巧 第6章接口与内部类 6.1接口 6.1.1接口的特性 6.1.2接口与抽象类 6.2对象克隆 6.3接口与回调 6.4内部类 6.4.1使用内部类访问对象状态 6.4.2内部类的特殊语法规则 6.4.3内部类是否实用、必要和安全 6.4.4局部内部类 6.4.5匿名内部类 6.4.6静态内部类 6.5代理 第7章图形程序设计 7.1 Swing概述 7.2创建框架 7.3框架定位 7.4在面板中显示信息 7.5 2D图形 7.6颜色 7.7为文本设定特殊字体 7.8图像 第8章事件处理 8.1、事件处理基础 8.1.1实例:处理按钮点击事件 8.1. 2建议使用内部类 8.1. 3将组件变成事件监听器 8.1.4实例:改变观感 8.1.5实例:捕获窗口事件 8.2 AWT事件继承层次 8.3 AWT的语义事件和低级事件 8.4低级事件类型 8.4.1键盘事件 8.4.2鼠标事件 8.4.3焦点事件 8.5动作 8.6多点传送 8.7实现事件源 第9章swing用户界面组件 9.1模型一视图一控制器设计模式 9.2布局管理器慨述 9.2.1边界布局 9.2.2面板 9.2.3网格布局 9.3文本输入 9.3.1文本域 9.3. 2标签与标签组件 9.3 3文本域变化跟踪 9.3.4密码域 9.3.5格式化的输入域 9.3.6文本区 9.4选择组件 9.4.1复选框 9.4.2单选按钮 9.4.3边界 9.4.4组合框 9.4.5滑块 9.4.6 JSpinner组件 9.5菜单 9.5.1菜单创建 9.5.2菜单项中的图标 9.5.3复选框和单选按钮菜单项 9.5.4弹出菜单 9.5.5快捷键和加速器 9.5.6启用和禁用菜单项 9.5.7工具栏 9 5.8工具提示 9.6复杂的布局管理 9.6.1箱式布局 9.6.2网格组布局 9.6.3弹簧布局 9.6.4不使用布局管理器 9.6.5定制布局管理器 9 6.6遍历顺序 9.7对话框 9.7.1选项对话框 9.7.2创建对话框 9.7.3数据交换 9.7.4文件对话框 9.7.5颜色选择器 第10章部署applet和应用程序 10.1 applet基础 10. 1. 1一个简单的applet 10.1. 1.2查看applet 10.1.3将应用程序转换为applet lO.1.4 applet的生命周期 10.1.5安全基础 10.1.6 applet中的弹出式窗口 10.2 applet的HTML标记和属性 11.5.1启用和禁用断言 11.5.2使用断言的建议 11.6调试技术 11. 6.1调试的常用技巧 11.6.2使用控制台窗口 11.6_3跟踪AWT事件 11.6.4 AWT的Robot类 11.7使用调试器 11.7.1 JDB调试器 11.7.2 Eclipse调试器 第12章流与文件 12.1流 12.2完整的流结构 12.2.1流过滤器的分层 12.2.2数据流 12.2.3随机存取文件流 12.2.4文本流 12.2.5字符集 12.2.6文本输出 12.2.7文本输入 12.3 ZIP文件流 12.4流的使用 12.4.1分隔符输出 12.4.2字符串记号处理器和带分隔符的文本 12.4.3读取带允隔符的输入 12.4.4 StringBuilder类 12.4.5随机存取流 12.5对象流 12.5.1存储可变类型的对象 12.5.2理解对象序列化文件格式 12.5.3保存对象引用问题的解决 12.5.4理解对象引用的输出格式 12.5.5修改默认的序列化机制 12.5.6单元素与类型安全枚举的序列化 12.5.7版本 12.5.8使用序列化进行克隆 12.6文件管理 12.7新的I/O 12.7.1内存映射文件 12.7.2缓冲区数据结构 12.7.3文件锁定 12.8正则表达式 13章泛型程序设计 13.1为什么要使用泛型程序设计 13.2简单泛型类的定义 13.3泛型方法 13.4类型变量的限定 13.5泛型代码和虚拟机 13.5.1翻译泛型表达式 13.5.2翻译泛型方法 13.5.3调用遗留代码 13.6约束与局限性 13.6.1基本类型 13.6.2运行时类型查询 13.6.3异常 13.6.4数组 13.6 5泛型类型的实例化 13.6 6静态上下文 13.6.7擦除后的冲突 13.7泛型类型的继承规则 13.8通配符类型 13.8.1通配符的超类型限定 13.8.2无限定通配符 13.8 3通配符捕获 13.9反射和泛型 13.9.1使用Class参数进行类型匹配 13.9. 2虚拟机中的泛型类型信息 附录AJava关键字 附录B更新的JDK 5.0代码 Java2核心技术II卷.高级特性 目录: 译者序 前言 第1章 多线程 1.1 什么是线程 1.2 中断线程 1.3 线程状态 1.4 线程属性 1.5 同步 1.6 阻塞队列 1.7 线程安全的集合 1.8 Callable和Future 1.9 执行器 1.10 同步器 1.11 线程和Swing工作器 第2章 集合 2.1 集合接口 2.2 具体的集合 2.3 集合框架 2.4 算法 2.5 遗留下来的集合 第3章 网络 3.1 连接到服务器 3.2 实现服务器 3.3 发送E-Mail 3.4 建立URL连接 3.5 高级套接字编程 第4章 数据库编程 4.1 JDBC的设计 4.2 结构化查询语言 4.3 安装JDBC驱动程序类型 4.4 JDBC的典型用法 4.5 执行查询操作 4.6 可滚动和可更新的结集 4.7 元数据 4.8 行集 4.9 事务 4.10 高级连接管理 4.11 LDAP概述 第5章 分布式对象 5.1 客户与服务器的角色 5.2 远程方法调用 5.3 配置远程方法调用 5.4 远程方法中的参数传递 5.5 服务器对象激活 5.6 JavaIDL与CORBA 5.7 远程方法调用与SOAP 第6章 高级Swing 第7章 高级AWT 第8章 JavaBean构件 第9章 安全 第10章 国际化 第11章 本地方法 第12章 XML 第13章 注释

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值