书名:Java 7并发编程实战手册
ISBN:978-7-115-33529-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [西] Javier Fernández González
译 申绍勇 俞黎敏
责任编辑 陈冀康
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©2012 Packt Publishing. First published in the English language under the title Java 7 Concurrency Cookbook All Rights Reserved.
本书由美国Packt Publishing 公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Java 7 在并发编程方面,带来了很多令人激动的新功能,这将使你的应用程序具备更好的并行任务性能。
本书是Java 7并发编程的实战指南,介绍了Java 7 并发API中大部分重要而有用的机制。全书分为9章,涵盖了线程管理、线程同步、线程执行器、Fork / Join 框架、并发集合、定制并发类、测试并发应用等内容。全书通过60多个简单而非常有效的实例,帮助读者快速掌握Java 7多线程应用程序的开发技术。学习完本书,你可以将这些开发技术直接应用到自己的应用程序中。
本书适合具有一定Java编程基础的读者阅读和学习。如果你是一名Java开发人员,并且想进一步掌握并发编程和多线程技术,并挖掘Java 7并发的新特性,那么本书是你的合适之选。
Javier Fernández González是一名有着超过10年Java技术经验的软件架构师。他曾担任过教师、研究员、程序员和分析员,现在是 Java 项目(特别是J2EE相关项目)的架构师。在担任教师期间,他在Java、J2EE和Struts 框架上有超过1000小时的教学时间。当研究员时,他曾在信息检索领域,用Java开发应用程序来处理大量的数据,此外他还是一些期刊文章和会议演示的合作者。近些年来,Javier在不同的领域(比如公共行政、保险、医疗保健、交通等)为不同的客户开发J2EE Web应用程序。目前,他在欧洲最大的咨询公司(Capgemini,凯捷)担任软件架构师,为保险公司开发和维护应用程序。
Edward E. Griebel Jr 在读小学的时候,通过 Apple 电脑上的 LOGO[1] 语言和在 VAX 上 的 Oregon Trail 首次接触计算机。
VAX(Virtual Address eXtension)是一种可以支持机器语言和虚拟地址的32位小型计算机。VAX最初由迪吉多电脑公司(DEC)在20世纪70年代初发明。
之后,Edward一直保持着对计算机的兴趣与追求,他毕业于 Bucknell 大学,获得了计算机工程学士学位。在Edward的第一份工作中,他很快意识到自己除了计算机编程技术以外其他的都不懂。在过去的 20 年间,Edward已经在证券交易、电信、支付处理、物联网等行业中磨砺过,担任过开发人员、团队的领导者、技术顾问和导师。目前在从事 Java EE 企业应用开发,他认为编写代码的日子是很快乐的,而不是苦闷的。
他说:
我要感谢我的妻子和三个孩子,他们允许我即使深夜也仍能在电脑前工作,直到很晚才休息而无法陪伴他们。
Jacek Laskowski 是一位非常专业的软件专家,擅长采用大量的商业和开源的解决方案来满足客户不同的业务需求。他开发应用程序、编写文章、辅导经验不足的工程师、录制视频、发布课程,同时,Jacek也是许多 IT 书籍的技术审校者。
Jacek专注于 Java EE、SOA(Service-Oriented Architecture)、BPM(Business Process Management)解决方案、OSGi 技术以及函数语言(Functional Programming),比如 Clojure 和 F# 语言。他也涉猎 Scala、Dart,并用 Java 和 HTML 5 进行原生的 Android 开发。
Jacek也是波兰首都、最大城市华沙的 Java 用户组(Warszawa Java User Group,Warszawa JUG) 的创始人与领导者。他还是 Apache 软件基金会(Apache Software Foundation)成员,Apache OpenEJB 和 Apache Geronimo项目管理委员会(Project Management Committee,PMC)会员和提交者(Committer)。Jacek经常在开发者大会上演讲。他的博客地址是 http://blog.japila.pl 和 http://blog.jaceklaskowski.pl,他的Twitter是@jaceklaskowski。
Jacek已经在 IBM 工作6 年多了,现在是 World-wide WebSphere 竞争对手迁移团队成员,并且也是通过 Level 2 认证的IT专家(译注:Level 2 即IBM公司内部的 Band 9 大师)。他协助客户从竞争对手的产品(通常是 Oracle WebLogic Server)迁移到 IBM WebSphere Application Server 上。
最近,Jacek加入了IBM技术研究院(IBM Academy of Technology)。
他说:
我要感谢我的家人——我的妻子 Agata 以及 3 个小孩 Iweta 、 Patryk 和 Maksym,有了他们坚定的支持、鼓励和耐心,我才取得这么多的成就!我爱你们!
Abraham Tehrani也是一位具有超过十年软件开始经验的开发人员和QA工程师。同时,他对质量和技术充满了热情!
他说:
我要感谢我的未婚妻,她给予我支持和爱的力量。我也要感谢我的朋友和家人,谢谢他们的全力支持。
[1] 注:LOGO 是一款计算机程序设计语言,在 1966 年由西摩尔•派普特 和Wally Feurzeig 在 BNN 设计,设计 LOGO 的初衷是为了向儿童教授计算机编程技术。
申绍勇
2003年研究生毕业于于中山大学,2007年加入IBM软件部,现任职WebSphere中间件资深售前工程师。擅长跨平台移动应用开发、移动应用整体解决方案设计,SOA及企业应用集成,对IBM的WebSphere MQ、Message Broker、Worklight等产品比较熟悉。在电信、银行、政府、证券、保险、物流等各个行业都有丰富的项目经验。
俞黎敏(ID:YuLimin,网名:阿敏总司令)
2008年7月1日加入国际商业机器(中国)有限公司广州分公司(IBM广州),担任软件部高级信息工程师、资深技术顾问,主要负责IBM WebSphere系列产品的技术支持工作,专注于产品新特性、系统性能调优、疑难问题诊断与解决。
开源爱好者,曾经参与Spring中文论坛组织“Spring 2.0 Reference”中文翻译的一审与二审工作,“满江红开放技术研究组织”的“Seam 1.2.1 Reference”中文翻译工作,并组织和完成“Seam 2.0 Reference”中文翻译工作。利用业余时间担任CSDN、CJSDN、Dev2Dev、Matrix、JavaWorldTW、Spring中文、WebSphereChina.net等Java论坛版主,在各大技术社区推动开源和敏捷开发做出了积极的贡献。参与审校与翻译的书籍有《Ajax设计模式》、《CSS实战手册》、《Hibernate实战》(第2版)、《Java脚本编程》、《Effective Java中文版第2版》、《Spring攻略》、《CSS实战手册第2版》、《Seam实战》、《REST实战中文版》、《Java 7程序设计》、《Servlet和JSP学习指南》等。
博客:http://blog.csdn.net/YuLimin或者 http://YuLimin.ItEye.com 微博:http://weibo.com/iAMin83567。
Java是一种计算机编程语言,拥有跨平台、面向对象、泛型编程的特性,广泛应用于企业级Web应用开发和移动应用开发。由Sun公司的James Gosling等人于1990年代初开发Java语言的雏形,最初被命名为Oak,于1995年5月以Java的名称正式发布。伴随着互联网的迅猛发展而发展,Java逐渐成为重要的网络编程语言。
自从Java 5.0 增加了最初由Doug Lea编写的高质量的、广泛使用的、并发实用程序 util.concurrent 并变成了 JSR-166的新包之后,在Java内置所提供的类库中,就提供了越来越多的并发编程的实用工具类。学习并掌握这些技术对于专注于Java并发编程的开发人员来讲是基本的功力,随着Java版本的不断更新与改进,开发人员可以通过Java新版本所带来的新特性,无需从头重新编写并发程序工具类。
本书作者Jacek Laskowski 是一位非常专业的软件专家,擅长采用大量的商业和开源的解决方案来满足客户不同的业务需求。他开发应用程序,编写文章,辅导经验不足的工程师,录制视频,发布课程,同时,他也是许多 IT 书籍的技术审校者。
本书是针对有Java编程语言基础的开发者的,需要已经熟悉普通的 Java 开发实践,如果掌握了的线程基本知识,那么阅读本书将更加得心应手。如果想进一步掌握并发编程和多线程技术,以及挖掘 Java 7并发的新特性,那么,本书正适合你,一边阅读一边动手实验掌握之。
Java 是一个并发平台,它提供了大量的类来执行 Java 程序中的并发任务。随着版本的不断更新发展,Java 不断地为程序员增加了并发编程的开发功能。本书覆盖了 Java 7 并发 API 中大部分重要而有用的机制,因此,你将能够直接在应用程序中使用它们,包括如下主题:
线程管理,通过基础的范例来讲解线程的创建、线程的执行以及线程的状态管理;
线程同步基础,为读者讲解如何使用低级的 Java 机制,比如采用 Lock 锁接口和 synchronized 关键字来同步代码;
线程同步辅助类,讲解如何使用 Java 的高级工具类来管理 Java 中的线程同步。比如介绍 Java 7 当中的 Phaser 类,用来同步被拆分成多个阶段的任务;
线程执行器(Thread Executor),讲解如何将线程管理委托给执行器(Executor)。执行器将为并发任务负责线程的创建、运行、管理并返回任务的结果;
Fork/Join框架(Fork/Join Framework),讲解如何使用 Java 7 新的一种特殊的执行器 Fork/Join框架。用来解决通过分治技术(Divide and Conquer Technique)将任务拆分成多个子任务的问题;
并发集合,讲解如何使用一些由 Java 语言提供的并发数据结构,从而避免在程序的实现中采用 synchronized 代码块;
定制并发类(Customizing Concurrency Classes),讲解如何根据需要来改编 Java 并发 API 中一些非常有用的类;
测试并发应用(Testing Concurrent Application),讲解如何获取 Java 7 并发 API 中最有用的结构的状态信息。讲解如何使用一些免费的工具来调试并发应用程序,比如:Eclipse、NetBeans IDE。或者用来检测应用程序中是否存在 Bug 的 FindBugs 开源框架。
附加信息没有包含在本书中,但是它可以通过如下链接免费下载:http://www.Java2Class.net/Java7ConcurrentCookbook/,讲解同步的概念,执行器框架(Executor Framework)和 Fork/Join 框架(Fork/Join Framework),并发数据结构,以及没有包含在相应章节里的并发对象的监控。
附录,并发编程设计(Concurrent Programming Design)也没有包含在本书当中,但是它可以通过如下链接免费下: http://www.Java2Class..net/http://www.Java2Class.net/Java7 ConcurrentCookbook/ ,讲解每一位程序员在开发并发应用程序时应当考虑的一些技巧。
本书旨在使你更全面、更专业地掌握Java 7并发API编程开发技术,但是它只是一本比较基础的书籍,如果你已经有多年的Java并发应用程序开发经验,或许可以通过快速的阅读或者直接找到自己所需要的新技术点。当阅读本书时,你会遇到许多需要动手进行验证的实例,可以利用本书附带的示例程序进行练习与实践。示例与答案下载地址为:http://www.packtpub.com/code_download/10250。
正如在翻译过程中发现原著的错误一样,虽然我们在翻译过程中竭力以求信、达、雅,但限于自身水平,必定仍会有诸多不足,还望各位读者不吝指正。大家可以通过访问我的博客 http://YuLimin.ItEye.com 或者发送电子邮件到 YuLimin@163.com进行互动。
关于术语的翻译,仍然沿用翻译Effective Java中文第2版时采用的术语表以及满江红开放技术研究组织翻译术语,请见http://yulimin.iteye.com/blog/272088。
感谢崔毅(http://cuiyi.javaeye.com/)对我在翻译中碰到的问题进行的深入讨论,并对本书翻译时所采用的术语进行了认真的磋商;感谢“满江红开放技术研究组织”的翻译同仁们在术语表讨论中提出许多中肯的建议;感谢满江红开源组织的曹晓钢提供的一些翻译注意事项和热情的帮助;感谢人民邮电出版社的编辑陈冀康认真仔细以及反复的校对与检查,辛苦了,谢谢!
本书由我组织翻译,申绍勇负责翻译第6至第8章,我负责翻译前言、第1章至第5章、附录并对全书所有章节进行全面审校,还负责对原文中的错误与作者进行沟通并加以修正,这里不得不提的一点就是,当你提交的勘误被确认后,英文原书出版社会送出一个免费的电子图书的购买优惠号,真是太感谢了!参与翻译与审校的还有:杨春花、崔毅、俞哲皆、张琬滢、蒋凌锋、魏伟、万国辉等,在此再次深表感谢。
本书章节安排合理,内容承上启下,但是需要边看书边动手做实验,才能充分理解并掌握Java 7 并发API带来的开发技术及新特性。快乐分享,实践出真知,最后,祝大家能够像我一样在阅读中享受本书带来的乐趣!
Read a bit and take it out, then come back read some more.
俞黎敏
2013年7月1日于广州
使用计算机时,可以同时做几件事情:可以一边听音乐,一边使用文字处理软件编辑文档,还可以阅读电子邮件。因为操作系统支持并发任务,从而使得这些工作得以同时进行。并发编程是一种平台和机制供多个任务或程序同时运行,并且互相通讯来交换数据(或者与其他任务进行同步等待)。
Java是一个并发平台,它提供了大量的类来执行Java程序中的并发任务。随着版本的不断更新发展,Java 不断地为程序员增加并发编程的开发功能。本书覆盖了Java 7并发API 中大部分重要而有用的机制,因此,能够直接在应用程序中使用它们,包括下列基本的线程管理:
◆ 线程同步机制
◆通过执行器创建和管理线程
◆ 通过 Fork/Join 框架提高应用程序的性能
◆ 并发编程的数据结构
◆ 根据需要调整一些并发类的默认行为
◆ 测试 Java 并发应用程序
第 1 章,线程管理(Thread Management)将为读者讲解如何通过线程来完成基本的操作。本章将通过基础的范例来讲解线程的创建、执行以及线程的状态管理。
第 2 章,线程同步基础(Basic Thread Synchronization)将为读者讲解如何使用基本的 Java 机制来同步代码。本章将详细阐述 Lock 锁接口和 synchronized 关键字的应用。
第 3 章,线程同步辅助类(Thread Synchronization Utilities)将为读者讲解如何使用 Java 的高级工具类来管理 Java 中的线程同步。本章使用 Java 7 当中的 Phaser 类,来同步被拆分成多个阶段的任务。
第 4 章,线程执行器(Thread Executor)将为读者讲解如何将线程管理委托给执行器(Executor)。执行器将为并发任务负责线程的创建、运行、管理并返回任务的结果。
第 5 章,Fork/Join框架(Fork/Join Framework)将为读者讲解如何使用 Java 7新引入的 Fork/Join框架。它是一种特殊的执行器,用来解决通过分治技术(Divide and Conquer Technique)将任务拆分成多个子任务的问题。
第 6 章,并发集合将为读者讲解如何使用一些由 Java 语言提供的并发数据结构。这些数据结构只能使用在并发编程中,从而避免在程序的实现中采用 synchronized 代码块。
第 7 章,定制并发类(Customizing Concurrency Classes)将为读者讲解如何根据需要来对Java 并发 API 中一些非常有用的类进行定制。
第 8 章,测试并发应用(Testing Concurrent Application)将为读者讲解如何获取 Java 7 并发 API 中最有用的结构的状态信息。读者也将学习如何使用一些免费的工具来调试并发应用程序,比如:Eclipse、NetBeans IDE。同时也将学习用来检测应用程序中是否存在 Bug 的 FindBugs 开源框架。
第 9 章,附加信息(Additional Information)没有包含在本书中,但是可以通过如下链接免费下载:http://www.packtpub.com/sites/default/files/downloads/Additional%20%20Information.pdf。这一章将为读者讲解同步的概念、执行器框架(Executor Framework)和 Fork/Join 框架(Fork/Join Framework)、并发数据结构,以及没有包含在相应章节里的并发对象的监控。
附录,并发编程设计(Concurrent Programming Design)也没有包含在本书当中,但是它可以通过链接免费下载:http://www.packtpub.com/sites/default/files/downloads/Concurrent %20%20Programming%20Design.pdf
附录将为读者讲解每一位程序员在开发并发应用程序时应当考虑使用的一些技巧。
为了阅读本书,首先需要读者有Java编程语言的基础知识,需要知道如何使用一种集成开发环境(Integrated Development Environment,IDE),比如Eclipse或者NetBeans,但是,这个不是必要的先决条件。
如果你是一名Java开发人员,想进一步掌握并发编程和多线程技术,以及挖掘 Java 7并发编程的新特性,那么,本书正适合你。你需要已经熟悉普通的Java开发实践,如果掌握了线程的基本知识,那么阅读本书将更加得心应手。
本书有大量不同的文本风格,以此区别不同的信息。这里有一些范例,以及它们的解释。
代码的文字说明将以如下形式说明“继承了Thread类,并覆盖了run()方法”。代码块的组织形式如下:
public Calculator(int number) {
this.number=number;
}
新术语(New Term)和重要的词(Important Word)将用粗体显示。在屏幕上、菜单里或者对话框上显示的词将用如下形式说明 “在菜单条的File菜单栏下通过New Project来创建一个新项目”。
【 警示或重要的备注将像这样在框中显示。】
【 技巧和窍门将像这样显示。】
我们一直欢迎读者们的反馈,让我们知道你对本书的想法,比如你喜欢或者不喜欢。读者反馈对我们来讲是非常重要的。发送反馈给我们相当简单,只需要发送电子邮件到feedback@packtpub.com信箱即可,在邮件的主题里提及本书的标题。
如果你是某一方面的技术专家,并且有兴趣编写和出版图书,可以通过http://www. packtpub.com/authors来获得作者指南。
现在你已是 Packt 图书的读者,我们有大量的方式可以让你的购买利益最大化。
在http://www.PacktPub.com网站上,通过已注册的账号可以下载到所有已经购买的Packt 图书的范例代码。如果你已经在别的地方购买了本书,你可以访问http://www.PacktPub.com/ support 并注册账号,我们将直接通过邮件把代码发送给你。
虽然我们已尽力确保内容的准确性,但错误仍有可能发生。如果你在我们的图书中发现错误,哪怕只是一个错误的文字或代码,如果你将此情况告知我们,我们将不胜感激。这样做,可以帮助其他读者为了这个错误而浪费时间,同时也帮助我们提高这本书后续版本的质量。如果你发现任何错误,请访问链接http://www.packtpub.com/support,选择书的标题,通过链接点击勘误提交表单,然后输入勘误表单的详细内容并提交。一旦核实,你提交的勘误将被接受,并将上传到网站的勘误列表中,或添加到标题下的勘误Errata一节中的现有勘误列表中。通过访问http://www.packtpub.com/support链接,选择书的标题,可以查看现有的勘误表。
所有媒体互联网上的版权材料通过各种媒体进行盗版是一个持续的问题。在 Packt,我们非常重视保护我们的版权和许可。如果你在互联网上遇到以任何形式非法复制和传播我们的作品,请立即向我们提供链接地址或网站名称,这样我们可以立即寻求解决办法。请通过 mailto:copyright@packtpub.com邮箱与我们联系,将怀疑盗版材料的链接告知我们。我们非常感谢能借助你的帮助来保护我们的作者,我们有能力为你带来有价值的内容。
对于本书,如果你有任何问题,可以通过mailto:questions@packtpub.com邮箱联系我们,我们将尽最大的努力来解决你的问题。
本章内容包括:
◆ 线程的创建和运行
◆ 线程信息的获取和设置
◆ 线程的中断
◆ 线程中断的控制
◆ 线程的休眠和恢复
◆ 等待线程的终止
◆ 守护线程的创建和运行
◆ 线程中不可控异常的处理
◆ 线程局部变量的使用
◆ 线程的分组
◆ 线程组中不可控异常的处理
◆ 使用工厂类创建线程
在计算机领域中,我们说的并发(Concurrency)是指一系列任务的同时运行。如果一台电脑有多个处理器或者有一个多核处理器,这个同时性(Simultaneity)是真正意义的并发;但是一台电脑只有一个单核处理器,这个同时性并不是真正的并发。
现代操作系统都允许多任务的并发执行:在听歌的时候,你可以同时阅读电子邮件,也可以同时阅读网页上的信息。这种并发是进程级(Process-Level)并发。但在一个进程内也可以有多个同时进行的任务。这种进程内并发的任务成为线程(Thread)。
与并发相关的另一个概念是并行(Parallelism)。与并发有不同的定义一样,并行也有不同的定义。一些学者认为并发是在单核处理器中使用多线程执行应用,与此同时你看到的程序执行只是表面的;相应的,他们认为并行是在多核处理器中使用多线程执行应用,这里的多核处理器可以是一个多核处理器,也可以是同一台电脑上的多个处理器。另一些学者认为并发执行应用的线程是非顺序执行的,相应的,他们认为并行是使用很多线程去简化问题,这些线程是按预定顺序执行的。
本章提供了很多例子来演示运用Java 7 API进行线程的基本操作。你将看到如何在Java程序里创建和运行线程,如何去控制线程的执行,如何把多个线程进行分组,以及如何去操作分组后的线程单元。
在本章中,我们将学习如何在Java程序中创建和运行线程。在Java语言中,线程跟其他所有元素一样,都是对象(Object)。Java提供了两种方式来创建线程:
◆ 继承Thread类,并且覆盖run()方法。
◆ 创建一个实现Runnable接口的类。使用带参数的Thread构造器来创建Thread对象。这个参数就是实现Runnable接口的类的一个对象。
在本章中,我们将使用第二种方法创建一个简单的程序,这个程序将创建并运行10个线程。每个线程用以计算和打印乘以1~10后的结果,即计算和打印乘法表。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个名为Calculator的类,它实现了Runnable接口。
public class Calculator implements Runnable {
2.声明一个名为number的私有(private)int属性。编写这个类的一个构造器,用来为属性number设置值。
private int number;
public Calculator(int number) {
this.number=number;
}
3.编写run()方法。这个方法用来执行我们创建的线程的指令,本范例中它将对指定的数字进行乘法表运算。
@Override
public void run() {
for (int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread. currentThread().getName(),number,i,i*number);
}
}
4.现在编写范例的主类。创建一个名为Main的类,创建的时候同时生成main()方法。
public class Main {
public static void main(String[] args) {
5.在main()方法中,创建一个执行10次的循环。在每次循环中创建一个Calculator对象,一个Thread对象,这个Thread对象使用刚创建的Calculator对象作为构造器的参数,然后调用刚创建的Thread对象的start()方法。
for (int i=1; i<=10; i++){
Calculator calculator=new Calculator(i);
Thread thread=new Thread(calculator);
thread.start();
}
6.运行程序,观察不同的线程是如何并行工作的。
下面的截图显示了程序的部分运行结果。可以看到我们创建的10个线程的运行情况,它们并行的执行既定任务,并将结果显示出来。
每个Java程序都至少有一个执行线程。当运行程序的时候,JVM将启动这个执行线程来调用程序的main()方法。
当调用Thread对象的start()方法时,另一个执行线程将被创建。因而在我们的程序中,每次调用start()方法时,都会创建一个执行线程。
当一个程序的所有线程都运行完成时,更明确的说,当所有非守护(non-daemon)线程都运行完成的时候,这个Java程序将宣告结束。如果初始线程(执行main()方法的线程)结束了,其余的线程仍将继续执行直到它们运行结束。如果某一个线程调用了System.exit()指令来结束程序的执行,所有的线程都将结束。
对一个实现了Runnable 接口的类来说,创建Thread对象并不会创建一个新的执行线程;同样的,调用它的run()方法,也不会创建一个新的执行线程。只有调用它的start()方法时,才会创建一个新的执行线程。
在本章简介中提到过还有另一种方法能够创建新的执行线程。编写一个类并继承Thread类,在这个类里覆盖run()方法,然后创建这个类的对象,并且调用start()方法,也会创建一个执行线程。
◆ 参见本书1.13节。
Thread类有一些保存信息的属性,这些属性可以用来标识线程,显示线程的状态或者控制线程的优先级。
ID:保存了线程的唯一标示符。
Name:保存了线程名称
Priority:保存了线程对象的优先级。线程的优先级是从1到10,其中1是最低优先级;10是最高优先级。我们并不推荐去改变线程的优先级,然而,在需要的时候,也可以这么做。
Status:保存了线程的状态。在Java中,线程的状态有6种:new、runnable、blocked、waiting、time waiting或者 terminated。
在本节,我们将编写程序为10个线程指定名称和优先级,并且输出它们的状态信息直到线程结束。每个线程都将计算一个数字的乘法表。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个名为Calculator的类,它实现了Runnable接口。
public class Calculator implements Runnable {
2.声明一个名为number的私有int属性。编写这个类的一个构造器,用来为属性number设置值。
private int number;
public Calculator(int number) {
this.number=number;
}
3.编写run()方法。这个方法用来执行我们创建的线程的指令,本范例中它将对指定的数字进行乘法表运算。
@Override
public void run() {
for (int i=1; i<=10; i++){
System.out.printf("%s: %d * %d = %d\n",Thread. currentThread().getName(),number,i,i*number);
}
}
4.编写范例的主类。创建一个名为Main的类,创建的时候同时生成main()方法。
public class Main {
public static void main(String[] args) {
5. 创建一个容量为10的线程数组,以用来存储线程;创建一个容量为10的Thread.State数组,以用来存放这10个线程运行时的状态。
Thread threads[]=new Thread[10];
Thread.State status[]=new Thread.State[10];
6.创建一个容量为10的Calculator对象数组,为每个对象都设置不同的数字,然后使用它们作为Thread构造器的参数来创建10个线程对象。并且将其中5个线程的优先级设置为最高,另外5个线程的优先级设置为最低。
for (int i=0; i<10; i++){
threads[i]=new Thread(new Calculator(i));
if ((i%2)==0){
threads[i].setPriority(Thread.MAX_PRIORITY);
} else {
threads[i].setPriorit87y(Thread.MIN_PRIORITY);
}
threads[i].setName("Thread "+i);
}
7.创建一个PrintWriter对象,用来把线程的状态演变写入到文件中。
try (FileWriter file = new FileWriter(".\\data\\log.txt");PrintWriter pw = new PrintWriter(file);){
8.把这10个线程的状态写入文件中。现在线程的状态是NEW。
for (int i=0; i<10; i++){
pw.println("Main : Status of Thread "+i+" : " +threads[i].getState());
status[i]=threads[i].getState();
}
9.开始执行10个线程。
for (int i=0; i<10; i++){
threads[i].start();
}
10.直到10个线程都运行完成,我们就可以查看他们的状态。所有任何一个线程的状态发生了变化,我们就会将它写入到文件中。
boolean finish=false;
while (!finish) {
for (int i=0; i<10; i++){
if (threads[i].getState()!=status[i]) {
writeThreadInfo(pw, threads[i],status[i]);
status[i]=threads[i].getState();
}
}
finish=true;
for (int i=0; i<10; i++){
finish=finish &&(threads[i].getState()==State.TERMINATED);
}
}
11.编写writeThreadInfo()方法,用来写下线程的ID、名称、优先级、旧的状态和新的状态。
private static void writeThreadInfo(PrintWriter pw, Thread
thread, State state) {
pw.printf("Main : Id %d - %s\n",thread.getId(),thread.getName());
pw.printf("Main : Priority: %d\n",thread.getPriority());
pw.printf("Main : Old State: %s\n",state);
pw.printf("Main : New State: %s\n",thread.getState());
pw.printf("Main : ************************************\n");
}
12.运行这个范例,然后打开log.txt文件来查看10个线程的状态演变。
下面的截屏是log.txt文件的一部分。在这个文件里,我们可以看到最高优先级的线程比最低优先级的线程结束得早。我们也可以看到每个线程的状态演变。
这个程序的乘法表运算显示在控制台上,每个线程的状态演变记录在log.txt里。这样你可以更清楚地看到线程的演变过程。
Thread类的属性存储了线程的所有信息。JVM使用线程的priority属性来决定某一刻由哪个线程来使用CPU,并且根据线程的情景为它们设置实际状态。
如果没有为线程指定一个名字,JVM将自动给它分配一个名字,格式是Thread-XX,其中XX是一组数字。线程的ID和状态是不允许被修改的,线程类没有提供setId()和setStatus()方法来修改它们。
通过本节,你已经学会了如何通过Thread对象访问属性信息。但是,也可以通过实现Runnable接口的对象来访问这些属性信息。如果一个线程是以Runnable对象为参数构建的,那么也可以使用Thread类的静态方法currentThread()来访问这个线程对象。
要注意的是,如果使用setPriority()方法设置的优先级不是从1到10这个范围内的值,运行时就会抛出IllegalArgumentException异常。
◆ 参见本书1.4节。
如果一个Java程序有不止一个执行线程,当所有线程都运行结束的时候,这个Java程序才能运行结束;更确切地说应该是所有的非守护线程运行结束时,或者其中一个线程调用了System.exit()方法时,这个Java程序才运行结束。如果你想终止一个程序,或者程序的某个用户试图取消线程对象正在运行的任务,就需要结束这个线程。
Java提供了中断机制,我们可以使用它来结束一个线程。这种机制要求线程检查它是否被中断了,然后决定是不是响应这个中断请求。线程允许忽略中断请求并且继续执行。
在本节中,我们将开发程序来创建一个线程,使其运行5秒钟后再通过中断机制强制使其终止。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个名为PrimeGenerator的类,并继承Thread类。
public class PrimeGenerator extends Thread{
2.覆盖run()方法,并在方法体内包含一个无限循环。在每次循环中,我们将处理从1开始的连续数。对每个数字,我们将计算它是不是一个质数,如果是的话就打印到控制台。
@Override
public void run() {
long number=1L;
while (true) {
if (isPrime(number)) {
System.out.printf("Number %d is Prime",number);
}
3.一个数字处理完后,调用isInterrupted()方法来检查线程是否被中断。如果isInterrupted()返回值是true,就写一个信息并且结束线程的执行。
if (isInterrupted()) {
System.out.printf("The Prime Generator has been Interrupted");
return;
}
number++;
}
}
4.实现isPrime()方法。isPrime()方法返回的是一个布尔值,如果接收到的参数是一个质数就返回true,否则就返回false。
private boolean isPrime(long number) {
if (number <=2) {
return true;
}
for (long i=2; i<number; i++){
if ((number % i)==0) {
return false;
}
}
return true;
}
5.现在我们来实现这个范例的主类Main,并且实现main()方法。
public class Main {
public static void main(String[] args) {
6.创建PrimeGenerator类的一个对象,并且运行这个线程对象。
Thread task=new PrimeGenerator();
task.start();
7.等待5秒钟后,中断PrimeGenerator线程。
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.interrupt();
8.运行范例并查看结果。
下面的截屏记录了上述范例的运行结果。通过这个图我们可以看到PrimeGenerator线程打印出的信息,并且看到当它被中断后就运行终止了。
Thread类有一个表明线程被中断与否的属性,它存放的是布尔值。线程的interrupt()方法被调用时,这个属性就会被设置为true。isInterrupted()方法只是返回这个属性的值。
还有一个方法可以检查线程是否已被中断,即Thread类的静态方法interrupted(),用来检查当前执行的线程是否被中断。
isInterrupted()和interrupted()方法有一个很大的区别。isInterrupted()不能改变interrupted属性的值,但是后者能设置interrupted属性为false。因为interrupted()是一个静态方法,更推荐使用isInterrupted()方法。
像之前提到的,线程可以忽略中断,但并不是预期的行为。
通过上一节,你已经学会了如何去中断执行中的线程,也学会了如何在线程对象中去控制这个中断。上一个例子中使用的机制,可以使用在线程很容易被中断的情况下。但是,如果线程实现了复杂的算法并且分布在几个方法中,或者线程里有递归调用的方法,我们就得使用一个更好的机制来控制线程的中断。为了达到这个目的,Java提供了InterruptedException异常。当检查到线程中断的时候,就抛出这个异常,然后在run()中捕获并处理这个异常。
在本节中,我们将实现线程类来完成下面的内容,它在一个文件夹及其子文件夹中寻找一个指定的文件。这个范例将示范如何用InterruptedException异常来控制线程的中断。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个名为FileSearch的类,并且实现Runnable接口。
public class FileSearch implements Runnable {
2.声明两个私有属性,一个是我们将要查找的文件名称,另一个是初始文件夹。实现这个类的构造器,用来初始化这两个属性。
private String initPath;
private String fileName;
public FileSearch(String initPath, String fileName) {
this.initPath = initPath;
this.fileName = fileName;
}
3.在FileSearch中实现run()方法。它将检查fileName属性是不是一个目录,如果是,就调用processDirectory()方法。processDirectory()方法会抛出InterruptedException异常,因此必须捕获并处理这个异常。
@Override
public void run() {
File file = new File(initPath);
if (file.isDirectory()) {
try {
directoryProcess(file);
} catch (InterruptedException e) {
System.out.printf("%s: The search has been interrupted",Thread.currentThread().getName());
}
}
}
4.实现directoryProcess()方法,这个方法会获取一个文件夹里的所有文件和子文件夹,并进行处理。对于每一个目录,这个方法将递归调用,并且用相应目录名作为传入参数。对于每个文件,这个方法将调用fileProcess()方法。处理完所有的文件和文件夹后,这个方法将检查线程是不是被中断了,如果是,就抛出InterruptedException异常。
private void directoryProcess(File file) throws InterruptedException {
File list[] = file.listFiles();
if (list != null) {
for (int i = 0; i < list.length; i++) {
if (list[i].isDirectory()) {
directoryProcess(list[i]);
} else {
fileProcess(list[i]);
}
}
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
5.实现processFile()方法。这个方法将比较当前文件的文件名和要查找的文件名,如果文件名匹配,就将信息打印到控制台。做完比较后,线程将检查是不是被中断了,如果是,它将抛出InterruptedException异常。
private void fileProcess(File file) throws InterruptedException
{
if (file.getName().equals(fileName)) {
System.out.printf("%s : %s\n",Thread.currentThread().getName() , file.getAbsolutePath());
}
if (Thread.interrupted()) {
throw new InterruptedException();
}
}
6.现在,我们实现这个范例的主类。实现一个包含main()方法的Main类。
public class Main {
public static void main(String[] args) {
7.创建FileSearch类的一个对象,并用它作为传入参数来创建一个线程对象,然后启动线程执行任务。
FileSearch searcher=new FileSearch("C:\\","autoexec.bat");
Thread thread=new Thread(searcher);
thread.start();
8.等待10秒钟,然后中断线程。
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
9.运行这个范例并查看结果。
下面的截屏记录了范例的运行结果。你可以看到当FileSearch对象检测到它被中断之后就结束了运行。
在本范例中,我们使用了Java异常来控制线程的中断。当运行这个范例时,程序将进入文件夹查找是否包含指定的文件。例如,如果要查找的文件夹目录结构是\b\c\d,这个程序将递归调用processDirectory()方法3次。不管递归调用了多少次,只要线程检测到它已经被中断了,就会立即抛出InterruptedException异常,然后继续执行run()方法。
与并发API相关的Java方法将会抛出InterruptedException异常,如sleep()方法。
◆ 参见本书1.4节。
有些时候,你需要在某一个预期的时间中断线程的执行。例如,程序的一个线程每隔一分钟检查一次传感器状态,其余时间什么都不做。在这段空闲时间,线程不占用计算机的任何资源。当它继续执行的CPU时钟来临时,JVM会选中它继续执行。可以通过线程的sleep()方法来达到这个目标。sleep()方法接受整型数值作为参数,以表明线程挂起执行的毫秒数。当线程休眠的时间结束了,JVM会分给它CPU时钟,线程将继续执行它的指令。
sleep()方法的另一种使用方式是通过TimeUnit枚举类元素进行调用。这个方法也使用Thread类的sleep()方法来使当前线程休眠,但是它接收的参数单位是秒,最后会被转化成毫秒。
在本节中,我们将开发程序来完成这样的内容:使用sleep()方法,每间隔一秒就输出实际时间。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个名为FileClock的类,并且实现Runnable接口。
public class FileClock implements Runnable {
2.实现run()方法。
@Override
public void run() {
3.编写一个执行10次的循环。在每个循环中,创建一个Date对象,并把它写入到文件中,然后调用TimeUnit类的SECONDS属性的sleep()方法来挂起线程一秒钟。这个值将让线程休眠大概1秒钟。sleep()方法会抛出InterruptedException异常,我们必须捕获并处理这个异常。最佳实践是,当线程被中断时,释放或者关闭线程正在使用的资源。
for (int i = 0; i < 10; i++) {
System.out.printf("%s\n", new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.printf("The FileClock has been interrupted");
}
}
}
4.实现范例的主类。创建一个名为FileMain的类并包含main()方法。
public class FileMain {
public static void main(String[] args) {
5.创建FileClock类的一个对象,并用它作为传入参数来创建一个Thread对象,然后运行这个线程。
FileClock clock=new FileClock();
Thread thread=new Thread(clock);
thread.start();
6.调用TimeUnit类的SECONDS属性的sleep()方法,休眠5秒钟。
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
};
7.中断FileClock线程。
thread.interrupt();
8.运行这个范例并且观察结果。
当运行这个范例时,你可以看到程序每间隔一秒钟就会输出实际的时间,接下来是FileClock线程已经被中断的信息。
当调用sleep()方法之后,线程会释放CPU并且不再继续执行任务。在这段时间内,线程不占用CPU时钟,所以CPU可以执行其他的任务。
如果休眠中线程被中断,该方法就会立即抛出InterruptedException异常,而不需要等待到线程休眠时间结束。
Java并发API还提供了另外一个方法来使线程对象释放CPU,即yield()方法,它将通知JVM这个线程对象可以释放CPU了。JVM并不保证遵循这个要求。通常来说,yield()方法只做调试使用。
在一些情形下,我们必须等待线程的终止。例如,我们的程序在执行其他的任务时,必须先初始化一些必须的资源。可以使用线程来完成这些初始化任务,等待线程终止,再执行程序的其他任务。
为了达到这个目的,我们使用Thread类的join()方法。当一个线程对象的join()方法被调用时,调用它的线程将被挂起,直到这个线程对象完成它的任务。
在本节中,我们将通过初始化资源的范例来学习join()方法。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建名为DataSourcesLoader的类,并且实现Runnable接口。
public class DataSourcesLoader implements Runnable {
2.实现run()方法。这个方法先显示一个表明它开始执行的信息,然后休眠4秒钟,再显示另一个信息表明已完成当前执行。
@Override
public void run() {
System.out.printf("Beginning data sources loading: %s\n",new Date());
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Data sources loading has finished: %s\n",new Date());
}
3.创建一个NetworkConnectionsLoader类,用以实现Runnable接口。实现run()方法的方式与DataSourcesLoade的run()方法类似,但是它休眠6秒钟。
4.创建一个包含main()方法的Main类。
public class Main {
public static void main(String[] args) {
5.创建一个DataSourcesLoader对象,并用它作为传入参数来创建一个线程。
DataSourcesLoader dsLoader = new DataSourcesLoader();
Thread thread1 = new Thread(dsLoader,"DataSourceThread");
6.创建一个NetworkConnectionsLoader对象,并用它作为传入参数来创建一个线程。
NetworkConnectionsLoader ncLoader = new NetworkConnectionsLoader();
Thread thread2 = new Thread(ncLoader,"NetworkConnectionLoader");
7.调用start()方法启动这两个线程对象。
thread1.start();
thread2.start();
8.使用join()方法等待两个线程的终止。join()方法会抛出InterruptedException异常,我们必须捕获并处理这个异常。
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
9.程序运行结束时,打印出信息。
System.out.printf("Main: Configuration has been loaded: %s\n",new Date());
10.运行程序并观察运行结果。
运行这个程序时,你会看到两个线程对象是如何运行的。DataSourcesLoader线程运行结束,NetworkConnectionsLoader线程也运行结束的时候,主线程对象才会继续运行并且打印出最终的信息。
Java提供了另外两种形式的join()方法:
join (long milliseconds)
join (long milliseconds, long nanos)
当一个线程调用其他某个线程的join()方法时,如果使用的是第一种join()方式,那么它不必等到被调用线程运行终止,如果参数指定的毫秒时钟已经到达,它将继续运行。例如,thread1中有这样的代码thread2.join(1000),thread1将挂起运行,直到满足下面两个条件之一:
◆ thread2运行已经完成;
◆ 时钟已经过去1000毫秒。
当两个条件中的任何一条成立时,join()方法将返回。
第二种join()方法跟第一种相似,只是需要接受毫秒和纳秒两个参数。
Java里有一种特殊的线程叫做守护(Daemon)线程。这种线程的优先级很低,通常来说,当同一个应用程序里没有其他的线程运行的时候,守护线程才运行。当守护线程是程序中唯一运行的线程时,守护线程执行结束后,JVM也就结束了这个程序。
因为这种特性,守护线程通常被用来做为同一程序中普通线程(也称为用户线程)的服务提供者。它们通常是无限循环的,以等待服务请求或者执行线程的任务。它们不能做重要的工作,因为我们不可能知道守护线程什么时候能够获取CPU时钟,并且,在没有其他线程运行的时候,守护线程随时可能结束。一个典型的守护线程是Java的垃圾回收器(Garbage Collector)。
在本节中,我们将通过范例学到如何创建守护线程,范例程序包含两个线程;一个是用户线程,它将事件写入到一个队列中;另一个是守护线程,它将管理这个队列,如果生成的事件超过10秒钟,就会被移除。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建Event类。这个类只存放满足本范例需要的信息。声明两个私有属性,一个日期类型的属性date;另一个字符串型的属性event。并生成这两个属性的读写方法。
2.创建WriterTask类,用以实现Runnable接口。
public class WriterTask implements Runnable {
3.声明一个存放Event对象的队列,并实现一个带参数的构造器,来初始化这个队列对象。
private Deque<Event> deque;
public WriterTask (Deque<Event> deque){
this.deque=deque;
}
4.实现线程的run()方法。它将执行100次循环。在每次循环中,都会创建一个新的Event对象,并放入到队列中,然后休眠一秒钟。
@Override
public void run() {
for (int i=1; i<100; i++) {
Event event=new Event();
event.setDate(new Date());
event.setEvent(String.format("The thread %s has generated an event",Thread.currentThread().getId()));
deque.addFirst(event);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
5.创建CleanerTask类并继承Thread类。
public class CleanerTask extends Thread {
6.声明存放Event对象的队列,并实现一个带参数的构造器,来初始化这个队列对象。同时,在这个构造器中,通过setDaemon()方法把这个线程设置为守护线程。
private Deque<Event> deque;
public CleanerTask(Deque<Event> deque) {
this.deque = deque;
setDaemon(true);
}
7.实现run()方法。它将无限制的重复运行,在每次运行中,将获取当前时间,并调用clean()方法。
@Override
public void run() {
while (true) {
Date date = new Date();
clean(date);
}
}
8.实现clean()方法。clean()将读取队列的最后一个事件对象,如果这个事件是10秒钟前创建的,就将它删除并且检查下一个。如果有事件被删除,clean()将打印出这个被删除事件的信息,也打印出队列的长度,这样,我们就可以看到程序的演化过程。
private void clean(Date date) {
long difference;
boolean delete;
if (deque.size()==0) {
return;
}
delete=false;
do {
Event e = deque.getLast();
difference = date.getTime() - e.getDate().getTime();
if (difference > 10000) {
System.out.printf("Cleaner: %s\n",e.getEvent());
deque.removeLast();
delete=true;
}
} while (difference > 10000);
if (delete){
System.out.printf("Cleaner: Size of the queue: %d\n",deque. size());
}
}
9.现在实现主类。创建一个包含main()方法的Main类。
public class Main {
public static void main(String[] args) {
10.创建一个队列对象Deque,用来存放事件。
Deque<Event> deque=new ArrayDeque<Event>();
11.创建三个WriterTask线程和一个CleanerTask线程,并启动它们。
WriterTask writer=new WriterTask(deque);
for (int i=0; i<3; i++){
Thread thread=new Thread(writer);
thread.start();
}
CleanerTask cleaner=new CleanerTask(deque);
cleaner.start();
12.运行程序并且查看结果。
对程序的运行输出进行分析之后,我们会发现,队列中的对象会不断增长直到30个,然后到程序结束,队列的长度维持在27~30之间。
这个程序有3个WriterTask线程,每个线程向列队写入一个事件,然后休眠1秒钟。在第一个10秒钟内,队列中有30个事件,直到3个WriterTask都休眠后,CleanerTask才开始执行,但是它没有删除任何事件。因为所有的事件都小于10秒钟。在接下来的运行中,CleanerTask每秒删除3个对象,同时WriterTask会写入3个对象,所以队列的长度一直介于27~30之间。
你可以不断调试WriterTask休眠的时间。如果使用一个更小的值,会发现CleanerTask将有更少的CPU时间,并且队列的长度将增加,因为CleanerTask没有删除对象。
setDaemon()方法只能在start()方法被调用之前设置。一旦线程开始运行,将不能再修改守护状态。
isDaemon()方法被用来检查一个线程是不是守护线程,返回值true表示这个线程是守护线程,false表示这个线程是用户线程。
在Java中有两种异常。
◆ 非运行时异常(Checked Exception):这种异常必须在方法声明的throws语句指定,或者在方法体内捕获。例如:IOException和ClassNotFoundException。
◆ 运行时异常(Unchecked Exception):这种异常不必在方法声明中指定,也不需要在方法体中捕获。例如:NumberFormatException。
因为run()方法不支持throws语句,所以当线程对象的run()方法抛出非运行异常时,我们必须捕获并且处理它们。当运行时异常从run()方法中抛出时,默认行为是在控制台输出堆栈记录并且退出程序。
好在,Java提供给我们一种在线程对象里捕获和处理运行时异常的一种机制。
在本节中,我们将通过范例学习这种机制。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.实现用来处理运行时异常的类。这个类实现UncaughtExceptionHandler接口并且实现这个接口的uncaughtException()方法。我们的范例将使用ExceptionHandler类的uncaughtException()方法打印出异常信息和抛出异常的线程代码。代码如下:
public class ExceptionHandler implements UncaughtExceptionHandler{
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("An exception has been captured\n");
System.out.printf("Thread: %s\n",t.getId());
System.out.printf("Exception: %s: %s\n",e.getClass(). getName(),e.getMessage());
System.out.printf("Stack Trace: \n");
e.printStackTrace(System.out);
System.out.printf("Thread status: %s\n",t.getState());
}
}
2.实现一个抛出运行时异常的线程类,命名为Task,它实现了Runnable接口,在实现run()方法时强制抛出运行时异常。例如,把一个String值转换成int值。
public class Task implements Runnable {
@Override
public void run() {
int numero=Integer.parseInt("TTT");
}
}
3.实现范例的主程序。实现一个包含main()方法的Main类。
public class Main {
public static void main(String[] args) {
4.创建一个Task对象,并用它作为传入参数来创建一个Thread对象。接着调用setUncaughtExceptionHandler()方法设置线程的运行时异常处理器,然后启动这个线程。
Task task=new Task();
Thread thread=new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
5.运行范例并查看运行结果。
下面的截屏记录了范例的运行结果。当异常抛出并被异常处理器捕获后,将在控制台打印出异常信息和抛出异常的线程代码。
当一个线程抛出了异常并且没有被捕获时(这种情况只可能是运行时异常),JVM检查这个线程是否被预置了未捕获异常处理器。如果找到,JVM将调用线程对象的这个方法,并将线程对象和异常作为传入参数。
如果线程没有被预置未捕获异常处理器,JVM将打印堆栈记录到控制台,并退出程序。
Thread类还有另一个方法可以处理未捕获到的异常,即静态方法setDefaultUncaught ExceptionHandler()。这个方法在应用程序中为所有的线程对象创建了一个异常处理器。
当线程抛出一个未捕获到的异常时,JVM将为异常寻找以下三种可能的处理器。
首先,它查找线程对象的未捕获异常处理器。如果找不到,JVM继续查找线程对象所在的线程组(ThreadGroup)的未捕获异常处理器,这将在“线程组中不可控异常的处理”一节中讲解。如果还是找不到,如同本节所讲的,JVM将继续查找默认的未捕获异常处理器。
如果没有一个处理器存在,JVM则将堆栈异常记录打印到控制台,并退出程序。
◆ 参见1.12节。
共享数据是并发程序最核心的问题之一,对于继承了Thread类或者实现了Runnable接口的对象来说尤其重要。
如果创建的对象是实现了Runnable接口的类的实例,用它作为传入参数创建多个线程对象并启动这些线程,那么所有的线程将共享相同的属性。也就是说,如果你在一个线程中改变了一个属性,所有线程都会被这个改变影响。
在某种情况下,这个对象的属性不需要被所有线程共享。Java并发API提供了一个干净的机制,即线程局部变量(Thread-Local Variable),其具有很好的性能。
本节中,我们将创建两个程序:第一个具有刚才提到的问题,另一个使用线程局部变量机制解决了这个问题。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例
1.使要实现的范例具有之前提到的共享问题。创建一个名为UnsafeTask的类,它实现了Runnable接口。声明一个私有的java.util.Date属性。
public class UnsafeTask implements Runnable{
private Date startDate;
2.实现run()方法,这个方法将初始化startDate属性,并且将值打印到控制台,让线程休眠一个随机时间,然后再次将startDate的值打印到控制台。
@Override
public void run() {
startDate=new Date();
System.out.printf("Starting Thread: %s : %s\n",Thread. currentThread().getId(),startDate);
try {
TimeUnit.SECONDS.sleep( (int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate);
}
3.实现这个有问题的应用程序的主程序。创建一个包含main()方法的Main类。这个方法将创建一个UnsafeTask类对象,用它作为传入参数创建10个线程对象并启动这10个线程,每个线程的启动间隔2秒。
public class Core {
public static void main(String[] args) {
UnsafeTask task=new UnsafeTask();
for (int i=0; i<10; i++){
Thread thread=new Thread(task);
thread.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
4.在下面的截屏中,你将看到这个程序执行的结果。每个线程有一个不同的开始时间,但是当它们结束时,三个线程都有相同的startDate属性值。
5.如之前提到的,我们将使用线程局部变量机制来解决这个问题。
6.创建一个SafeTask类,用以实现Runnable接口
public class SafeTask implements Runnable {
7.声明一个ThreadLocal对象。这个对象是在initialValue()方法中隐式实现的。这个方法将返回当前日期。
private static ThreadLocal<Date> startDate= new
ThreadLocal<Date>() {
protected Date initialValue(){
return new Date();
}
};
8.实现run()方法。它跟UnsafeTask类的run()方法实现了一样的功能,但是访问startDate属性的方式改变了。
@Override
public void run() {
System.out.printf("Starting Thread: %s : %s\n",Thread. currentThread().getId(),startDate.get());
try {
TimeUnit.SECONDS.sleep((int)Math.rint(Math.random()*10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("Thread Finished: %s : %s\n",Thread. currentThread().getId(),startDate.get());
}
9.这个范例的入口类与第一个范例一样,只是创建并作为参数参入的Runnable类对象不同而已。
10.运行范例,并分析两个范例之间的不同。
在下面的截屏中,你将看到安全线程类的执行结果。现在,这3个线程对象都有它们自己的startDate属性值。
线程局部变量分别为每个线程存储了各自的属性值,并提供给每个线程使用。你可以使用get()方法读取这个值,并用set()方法设置这个值。如果线程是第一次访问线程局部变量,线程局部变量可能还没有为它存储值,这个时候initialValue()方法就会被调用,并且返回当前的时间值。
线程局部变量也提供了remove()方法,用来为访问这个变量的线程删除已经存储的值。Java并发API包含了InheritableThreadLocal类,如果一个线程是从其他某个线程中创建的,这个类将提供继承的值。如果一个线程A在线程局部变量已有值,当它创建其他某个线程B时,线程B的线程局部变量将跟线程A是一样的。你可以覆盖childValue()方法,这个方法用来初始化子线程在线程局部变量中的值。它使用父线程在线程局部变量中的值作为传入参数。
Java并发API提供了一个有趣的功能,它能够把线程分组。这允许我们把一个组的线程当成一个单一的单元,对组内线程对象进行访问并操作它们。例如,对于一些执行同样任务的线程,你想控制它们,不管多少线程在运行,只需要一个单一的调用,所有这些线程的运行都会被中断。
Java提供ThreadGroup类表示一组线程。线程组可以包含线程对象,也可以包含其他的线程组对象,它是一个树形结构。
在本节中,我们学习并使用ThreadGroup对象类开发一个简单的范例:创建10个线程并让它们休眠一个随机时间(例如模拟一个查询),当其中一个线程查找成功的时候,我们将中断其他的9个线程。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1. 创建一个名为Result的类。它存储先执行完的线程。声明一个私有字符串变量name,并生成读写这个值的方法。
2.创建一个名为SearchTask的类,它实现了Runnable接口。
public class SearchTask implements Runnable {
3.声明一个Result类的私有属性,并实现带参数的构造器(Constructor),来为这个属性设置值。
private Result result;
public SearchTask(Result result) {
this.result=result;
}
4.实现run()方法。它将调用doTask()方法,并等待它完成或者抛出一个InterruptedException异常。run()方法也将打印出线程的开始、结束或者中断等信息。
@Override
public void run() {
String name=Thread.currentThread().getName();
System.out.printf("Thread %s: Start\n",name);
try {
doTask();
result.setName(name);
} catch (InterruptedException e) {
System.out.printf("Thread %s: Interrupted\n",name);
return;
}
System.out.printf("Thread %s: End\n",name);
}
5.实现doTask()方法。它创建Random对象来生成一个随机数,并用它做为传入参数调用sleep()方法。
private void doTask() throws InterruptedException {
Random random=new Random((new Date()).getTime());
int value=(int)(random.nextDouble()*100);
System.out.printf("Thread %s: %d\n",Thread.currentThread(). getName(),value);
TimeUnit.SECONDS.sleep(value);
}
6.创建一个包含main()方法的主类Main。
public class Main {
public static void main(String[] args) {
7.创建一个标识为Searcher的线程组对象。
ThreadGroup threadGroup = new ThreadGroup("Searcher");
8.创建一个Result 对象,并用它作为传入参数创建一个SearchTask对象。
Result result=new Result();
SearchTask searchTask=new SearchTask(result);
9.使用创建的SearchTask对象作为传入参数创建10个线程对象。当调用线程的构造器时,第一个参数是ThreadGroup对象,第二个参数是SearchTask对象。
for (int i=0; i<5; i++) {
Thread thread=new Thread(threadGroup, searchTask);
thread.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
10.通过list()方法打印线程组对象的信息。
System.out.printf("Number of Threads: %d\n",threadGroup. activeCount());
System.out.printf("Information about the Thread Group\n");
threadGroup.list();
11.通过activeCount()方法获取线程组包含的线程数目,通过 enumerate()方法获取线程组包含的线程列表。这两个方法可以帮助我们获取每个线程的信息,如线程的状态。
Thread[] threads=new Thread[threadGroup.activeCount()];
threadGroup.enumerate(threads);
for (int i=0; i<threadGroup.activeCount(); i++) {
System.out.printf("Thread %s: %s\n",threads[i]. getName(),threads[i].getState());
}
12.调用waitFinish()方法,我们将在下面实现这个方法。它将等到线程组的第一个线程运行结束。
waitFinish(threadGroup);
13.使用interrupt()方法中断这个组中的其余线程。
threadGroup.interrupt();
14.实现waitFinish()方法。activeCount()方法被用来检测是否有线程运行结束。
private static void waitFinish(ThreadGroup threadGroup) {
while (threadGroup.activeCount()>9) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15.运行范例并查看运行结果。
在下面的截屏中,你会看到list()方法的输出及每个线程对象的状态。
线程组类存储了线程对象和关联的线程组对象,并可以访问它们的信息(例如状态),将执行的操作应用到所有成员上(例如中断)。
ThreadGroup类有很多方法,它的API文档提供了所有方法的完整解释。
提供应用程序中对错误情景的管理,是编程语言很重要的一面。和几乎所有的现代编程语言一样,Java语言也实现了通过异常管理机制来处理错误情景,它提供了很多类来表示不同的错误。当错误情景发生时,Java类将抛出这些异常。你可以使用这些异常,或者实现自己的异常,来管理类中的错误。
Java也提供了捕获和处理这些异常的机制。有的异常必须被捕获,或者必须使用方法的throws声明再次抛出,这类异常叫做非运行时异常。还有一类异常叫做运行时异常,它们不需要被捕获或者声明抛出。
在本章1.5节中,我们学习了如何在线程对象中处理非捕获异常。
另一种可行的做法是,建立一个方法来捕获线程组中的任何线程对象抛出的非捕获异常。
本节中,我们将通过范例学习这种异常处理的方法。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建一个MyThreadGroup类,并继承ThreadGroup。必须声明带参数的构造器,因为ThreadGroup没有默认的不带参数的构造器。
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
2.覆盖uncaughtException()方法。当线程组中的任何线程对象抛出异常的时候,这个方法就会被调用。在这里,这个方法将打印异常信息和抛出异常的线程代码到控制台,还将中断线程组中的其他线程。
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.printf("The thread %s has thrown an Exception\n",t. getId());
e.printStackTrace(System.out);
System.out.printf("Terminating the rest of the Threads\n");
interrupt();
}
3.创建一个Task类,它实现了Runnable接口。
public class Task implements Runnable {
4.实现run()方法。在这个方法里,我们要触发AritmethicException异常。为了达到目标,我们用1000除以一个随机数,当随机数生成器生成0,异常将被抛出。
@Override
public void run() {
int result;
Random random=new Random(Thread.currentThread().getId());
while (true) {
result=1000/((int)(random.nextDouble()*1000));
System.out.printf("%s : %d\n",Thread.currentThread(). getId(),result);
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d : Interrupted\n",Thread. currentThread().getId());
return;
}
}
}
5.实现这个范例的主程序,并且实现main()方法。
public class Main {
public static void main(String[] args) {
6.创建一个MyThreadGroup线程组类对象。
MyThreadGroup threadGroup=new MyThreadGroup("MyThreadGroup");
7.创建一个Task类对象。
Task task=new Task();
8.用刚创建的两个对象作为传入参数,创建两个线程对象。
for (int i=0; i<2; i++){
Thread t=new Thread(threadGroup,task);
t.start();
}
9.运行范例并查看运行结果。
当运行范例的时候,你会看到当一个线程对象抛出了异常,其余的线程对象都被中断。
当线程抛出非捕获异常时,JVM将为这个异常寻找3种可能的处理器。
首先,寻找抛出这个异常的线程的非捕获异常处理器,参见1.9节。如果这个处理器不存在,JVM继续查找这个线程所在的线程组的非捕获异常处理器,这也是本节学习的知识。如果也不存在,JVM将寻找默认的非捕获异常处理器,参见1.9节。
如果这些处理器都不存在,JVM将把堆栈中异常信息打印到控制台,并且退出这个程序。
◆ 参见本书1.12节。
工厂模式是面向对象编程中最常使用的模式之一。它是一个创建者模式,使用一个类为其他的一个或者多个类创建对象。当我们要为这些类创建对象时,不需再使用new构造器,而使用工厂类。
使用工厂类,可以将对象的创建集中化,这样做有以下的好处:
◆ 更容易修改类,或者改变创建对象的方式;
◆ 更容易为有限资源限制创建对象的数目。例如,我们可以限制一个类型的对象不多于n个。
◆ 更容易为创建的对象生成统计数据。
Java提供了ThreadFactory接口,这个接口实现了线程对象工厂。Java并发API的高级工具类也使用了线程工厂创建线程。
在本节中,我们将学习如何通过实现ThreadFactory接口来创建线程对象,用以生成个性化名称的线程并且保存这些线程对象的统计信息。
本节的范例是在Eclipse IDE里完成的。无论你使用Eclipse还是其他的IDE(比如NetBeans),都可以打开这个IDE并且创建一个新的Java工程。
按照接下来的步骤实现本节的范例。
1.创建名为MyThreadFactory的类,并且实现ThreadFactory接口。
public class MyThreadFactory implements ThreadFactory {
2.声明3个属性:整型数字counter,用来存储线程对象的数量;字符串name,用来存放每个线程的名称;字符串列表stats,用来存放线程对象的统计数据。同时实现带参数的构造器来初始化这3个属性。
private int counter;
private String name;
private List<String> stats;
public MyThreadFactory(String name){
counter=0;
this.name=name;
stats=new ArrayList<String>();
}
3.实现newThread()方法。这个方法以Runnable接口对象为参数,并且返回参数对应的线程对象。这里我们创建一个线程对象并生成线程名称,保存统计数据。
@Override
public Thread newThread(Runnable r) {
Thread t=new Thread(r,name+"-Thread_"+counter);
counter++;
stats.add(String.format("Created thread %d with name %s on %s\n",t.getId(),t.getName(),new Date()));
return t;
}
4.实现getStatistics()方法,返回一个字符串对象,用来表示所有线程对象的统计数据。
public String getStats(){
StringBuffer buffer=new StringBuffer();
Iterator<String> it=stats.iterator();
while (it.hasNext()) {
buffer.append(it.next());
buffer.append("\n");
}
return buffer.toString();
}
5.创建名为Task的类并且实现Runnable接口。在这个范例中,线程除了只休眠1秒钟之外,不做任何事情。
public class Task implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
6.创建范例的主程序。创建一个包含main()方法的主类Main。
public class Main {
public static void main(String[] args) {
7.创建一个MyThreadFactory对象,创建一个Task对象。
MyThreadFactory factory=new MyThreadFactory("MyThreadFactory");
Task task=new Task();
8.使用工厂类MyThreadFactory创建10个线程对象,并且启动它们。
Thread thread;
System.out.printf("Starting the Threads\n");
for (int i=0; i<10; i++){
thread=factory.newThread(task);
thread.start();
}
9.将线程工厂的统计打印到控制台。
System.out.printf("Factory stats:\n");
System.out.printf("%s\n",factory.getStats());
10.运行范例并且查看运行结果。
ThreadFactory接口只有一个方法,即newThread,它以Runnable接口对象作为传入
参数并且返回一个线程对象。当实现ThreadFactory接口时,必须实现覆盖这个方法。大多数基本的线程工厂类只有一行,即:
return new Thread(r);
可以通过增加一些变化来强化实现方法覆盖。
◆ 创建一个个性化线程,如本范例使用一个特殊的格式作为线程名,或者通过继承Thread类来创建自己的线程类;
◆ 保存新创建的线程的统计数据,如本节的范例那样;
◆ 限制创建的线程的数量;
◆ 对生成的线程进行验证;
◆ 更多你可以想到的。
使用工厂设计模式是一个很好的编程实践,但是,如果是通过实现ThreadFactory接口来创建线程,你必须检查代码,以保证所有的线程都是使用这个工厂创建的。
◆ 参见7.4节。
◆ 参见7.5节。