书名:Java 9 并发编程实战
ISBN:978-7-115-50586-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [西] 哈维尔•费尔南德兹•冈萨雷斯
(Javier Fernández González )
译 ETO翻译小组
责任编辑 吴晋瑜
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright © Packt Publishing 2017. First published in the English language under the title Java 9 Concurrency Cookbook-Second Edition-(9781787124417). All rights reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
本书紧紧围绕Java 9并发类库和机制,由浅入深地讲解了Java 9并发编程的知识,并以案例的形式展现如何在真实需求中使用这些类库。
本书共11章。第1章到第4章主要介绍线程管理、Java同步代码的基本机制、线程间同步的高级工具、线程执行器等内容;第5章介绍fork/join框架的用法;第6章介绍流的相关知识以及Java 9中用来实现反应式流的接口;第7章到第9章介绍如何使用Java提供的并发数据结构,以及如何根据需要扩展Java并发API中最常用的类等内容;第10章和第11章就一些概念和开发注意事项进行拓展,包括并发数据、监控并发对象等内容。
本书给出了完整的案例开发步骤和代码,可以让读者直面程序运行过程,剖析原理、体会细节,适合对Java编程感兴趣的读者阅读。
用户在使用计算机时,可以一次处理多件事情,例如,在文字处理器中编辑文字和阅读邮件的同时,还可以听音乐。之所以可以这样,是因为操作系统支持多任务的并发执行。并发编程是指使用平台提供的一些元素和机制,使多个任务可以同时进行,并且相互通信实现数据交换和同步。Java是一个并发平台,它提供很多在Java程序中执行并发任务的类。在每个版本中,Java都会为开发者增加一些功能,便于并发程序的开发。本书涵盖了Java 9并发API中最重要和实用的机制,以便于读者在程序中直接应用。这些机制包括:
第1章介绍线程的基本操作。通过基本案例介绍线程的创建、执行、状态及管理。
第2章涵盖Java同步代码的基本机制。这一章会详细介绍Lock和synchronized关键字。
第3章介绍在Java中线程间同步的高级工具,主要详解如何使用Phaser类同步多阶段任务。
第4章阐述如何将线程管理委派给执行器,包括线程运行、管理、获取并发任务执行结果。
第5章阐述fork/join框架的使用。该框架是由执行器提供的一种特殊的框架,旨在使用分而治之技术将任务分割为更小的子任务。
第6章阐述如何创建流并使用中间和终端操作来并行且高效地处理一个大数据集合。Java 8引入了流这一工具,Java 9则添加了部分新接口来实现反应式流。
第7章阐述如何使用Java提供的部分并发数据结构。这些数据结构可以用在并发程序中来规避同步代码块的使用。
第8章阐述如何根据用户的需要扩展Java并发API中最常用的部分类。
第9章阐述如何获取Java 7并发API中最常用的数据结构的一些状态信息。读者可以了解到如何使用一些免费工具(如Eclipse、NetBeans IDE或是FindBugs)来调试并发程序和找出程序中可能的bug。
第10章阐述各章中对于同步、Executor、fork/join框架、并发数据结构和监控并发对象等未包含的一些概念。
第11章阐述程序员在开发并发程序时的一些注意事项。
为了能更好地学习本书,你需要了解与Java编程语言相关的基本知识,此外还需要了解如何至少会使用一种IDE,如Eclipse或是NetBeans,但这并不是一个必要条件。
如果你是一位有兴趣提高并发编程和多线程知识水平并乐于发现Java 8和Java 9并发新特性的Java开发者,那么这本书就是为你准备的。如果你已经熟悉了一般的Java开发实践,那么掌握线程的基本知识将是一个优势。
在本书中,你会发现很多频繁出现的标题(项目准备、案例实现、结果分析、其他说明、参考阅读)。
为了清楚说明如何完成一个案例,我们将采用以下形式。
本部分介绍案例中可预见的内容,并介绍如何设置所需的任何软件或初步设置。
本部分包含完成一个案例所需的步骤。
本部分通常会对“案例实现”中所有发生的事情进行详细解释。
本部分包含有关案例的其他信息,以便让读者更熟悉实战。
本部分给出了有助于了解案例的其他信息的有关章节。
在本书中,会出现许多文本样式以区分不同类型的信息。下面是这些样式的一些示例及其含义。
代码单词的文本、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入内容和编程短文将按如下形式进行显示,如:“执行main()
方法的代码。”
代码以如下样式展示:
Thread task=new PrimeGenerator();
task.start();
新名词和重要的词以黑体显示。例如,在菜单或对话框中,你在计算机屏幕上看到的单词会显示在文本中,如“通过单击〖〓strong〓〗File〖〓/strong〓〗菜单下的〖〓strong〓〗New Project〖〓/strong〓〗选项创建新项目”。
警告或重要提示将在此文本框中展示。
提示和技巧将在此文本框中展示。
随着移动互联网用户量的快速增长以及大数据、人工智能的兴起,开发者对程序的并发要求不断提高,并发研究与应用也成了行业内的热点与难点。因此,第一次接触本书,我们便产生了浓厚的兴趣,大家一拍即合,于是便趁热打铁地着手翻译工作。
本书的翻译工作耗时数月,为了能给大家展现最新、最完善的内容,保证翻译质量,我们分工协作,采用软件工程里的项目管理方式,分工翻译、交叉校对、提问题,不断推进翻译的进展。本书翻译工作的顺利完成离不开每位团队成员的辛勤付出。翻译期间,浮白(王利东)经历了紧张的项目迭代工作,为保证翻译的质量,他利用春节假期修改书稿;小护士(李梓峰)广博的见识让人赞叹,为翻译工作提供了扎实的依据;千山(徐江溢)和漫游鹰(夏钰辉)对内容的深刻理解和对翻译工作的认真态度让人敬佩。在翻译过程中,我们忠于原著,力求做到译文与原著相贴合,力求展现作者对Java并发的理解。我们对本书每个案例进行了真实测试,对于原著中存在的问题进行了小心求证,并就书中存在的相关问题与作者进行了积极沟通。但限于翻译水平有限,书中难免会有不足之处,敬请广大读者指正。
我热爱技术,在参加工作之初,我就对自己说,要在技术的道路上一直走下去。但理想与现实往往是背离的,生活、工作和自我,都像失控的火车,带我去到未知的地方。
直到半年前,跟几位仁兄一起翻译本书的工作,让我重新唤起内心对技术的渴望。这段时间,我经历了一直工作到凌晨的日子。2018年的春节,我推掉了所有应酬,专注于翻译,终于完成翻译工作。
感谢几位小伙伴,你们在我严重延期时默默地支持我,这对我很重要。感谢我的家人,你们的付出让我能安心地工作。
——王利东 花名:浮白
从业近5年,这是我首次参与技术书的翻译工作,新奇的同时也会心有忐忑。翻译期间,因为自身水平和其他事情曾一度导致进度滞后,所幸不辱使命,我还是在最后期限前顺利完成了翻译工作。
我在西小口开始本书的翻译,在后厂村完成本书的交付。我曾在夜深时对着计算机屏幕苦苦思索,也曾在春节万家灯火时挑灯奋战。我相信,这些经历终会成为自己技术成长道路上难忘的回忆。
感谢队友的支持、理解,为他们极高的专业水准点赞!也感谢队长和瑜姐提供了本次难得的机会。最后,特别感谢我的女朋友在我翻译工作完成后完美现身,让我可以全身心地投入到翻译工作中。
——夏钰辉 花名:漫游鹰
在历时两个月的翻译历程中,有喜有忧,除了看原著撰写译文以外,还有很多团队的沟通工作需要做好,例如,如何让专业名词有统一的翻译、如何跟踪译文的错漏修改、如何与成员讨论细节的处理。在这里,我要感谢广大读者的支持,感谢团队成员的无私包容,也感谢自己内心的那份坚持。
——李梓峰 花名:小护士
工作多年来,一直被Java语言的魅力深深吸引。庞大的Java开发生态,可移植、高性能、多线程、动态性、平台无关等优秀特性以及丰富强大的各类开发者社区,让Java语言久盛不衰。出于对Java语言的热爱,我开始一头扎入Java语言特性、多线程编程等许多有趣的领域,进而更加深刻地体会到Java语言的精妙和有趣。
机缘巧合,我有幸参与了这次难得的技术书翻译工作,有幸和这么一群优秀的伙伴一起工作,和大家一起在建设Java社区的道路上尽一份自己的力量。我依旧记得中山公园灯火辉煌的夜晚,记得在屏幕前苦苦思索的自己,也同样记得天南地北的小伙伴们在微信群里讨论如何“信、达、雅”地翻译拗口的语句,又或者讨论各类Java语言的细节。在此过程中,我受益匪浅。回首望去,虽然已经工作许多年了,但依旧不敢言精通任何一样东西。敬畏技术!敬畏人生!学无止境!
深深地感谢一起共事的小伙伴们,感谢成长道路上和我一起前行的亲爱的你们,以及人类科技发展道路上每一个默默付出的人,也诚挚地希望每一个阅读此书的人可以有所收获,有所感悟。
——徐江溢 花名:千山
每晚回到家中,关上房门,世界便安静下来;每次台灯下伏案,翻开本书,时间便停止下来。每行文字中的困顿,总在反复琢磨中豁然开朗;每个翻译的字词,总在小心求证中流淌出来。
我欣喜于每一次书中精妙的案例在大脑中演化,召唤我心中的灵感,带给我无限的思考;我沉醉于思考与翻译,希望把这份收获和喜悦传递给读者。
非常荣幸和各位小伙伴一起合作,浮白在春节孜孜不倦,钰辉找到了女朋友……这背后的故事,也让我满心感动。
——孙益超 花名:东方
ETO翻译小组
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书将为读者提供源代码。
读者可以在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,将赠与您异步社区的100积分(积分可用于在异步社区兑换优惠券、样书或奖品)。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的标志。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
在计算机世界中,并发是指一系列相互无关的任务在一台计算机上同时运行。对于有多个处理器或者多核处理器的计算机来说,这个同时性是真实发生的。然而,对于只有单核处理器的计算机来说,它仅仅是表面现象。
所有现代操作系统均支持并发地执行任务。用户可以在听音乐或者浏览网页的同时阅读邮件。这种并发是进程级别的并发。在同一进程内,也可以有多种同时运行的子任务,我们将这些并发的子任务称为线程。与并发性有关的另一个概念是并行性(parallelism)。虽然它与并发性的概念不同,但是有一定联系。一些学者认为,当多线程应用程序运行在单核处理器上时,程序就是并发运行的;当多线程应用程序运行在多个处理器或者多核处理器上时,程序就是并行运行的。还有一些学者认为,多线程应用程序的线程执行顺序若不是预先定义的,程序就是并发运行的;如果多线程应用程序的线程按照指定顺序执行,那么这个程序就是并行运行的。
本章介绍了如何使用Java 9 API来进行基本的线程操作,包括创建和运行线程、处理线程内抛出的异常、将线程分组,并将分组作为一个整体处理组内的线程。
本节介绍如何使用Java API对线程进行基本的操作。与Java语言中的基本元素一样,线程也是对象(Object)。在Java中,创建线程的方法有以下两种。
Thread
类,然后重写run()
方法。Runnable
接口的类并重写run()
方法,然后创建该类的实例对象,并以其作为构造参数去创建Thread
类的对象。建议首选这种方法,因为它可以带来更多的扩展性。在本节中,我们将采用第二种方法创建线程,然后学习如何改变线程的属性。Thread
类包含如下一些信息属性,它们能够辅助区分不同的线程、反映线程状态、控制其优先级等。
Thread
对象的优先级。在Java 9中,线程优先级的范围为1~10,其中1表示最低优先级,10表示最高优先级。通常不建议修改线程的优先级。线程优先级仅供底层操作系统作为参考,不能保证任何事情,如果一定要修改,请知晓优先级仅仅代表了一种可能性。Thread.State
枚举中定义这些状态:NEW
、RUNNABLE
、BLOCKED
、WAITING
、TIMED_WAITING
和TERMINATED
。这些状态的具体意义如下。
NEW:
线程已经创建完毕但未开始执行。RUNNABLE:
线程正在JVM中执行。BLOCKED:
线程处于阻塞状态,并且等待获取监视器。WAITING:
线程在等待另一个线程。TIMED_WAITING:
线程等待另一个线程一定的时间。TERMINATED:
线程执行完毕。本节将在一个案例中创建10个线程来找出20000以内的奇数。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据如下步骤实现本案例。
1.创建一个名为Calculator
的类,并实现Runnable
接口:
public class Calculator implements Runnable {
2.实现run()
方法。在这个方法中,存放着线程将要运行的指令。在这里,这个方法用来计算20000以内的奇数:
@Override
public void run() {
long current = 1L;
long max = 20000L;
long numPrimes = 0L;
System.out.printf("Thread '%s': START\n",
Thread.currentThread().getName());
while (current <= max) {
if (isPrime(current)) {
numPrimes++;
}
current++;
}
System.out.printf("Thread '%s': END. Number of Primes: %d\n",
Thread.currentThread().getName(), numPrimes);
}
3.实现辅助方法isPrime()
。该方法用于判断一个数是否为奇数:
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;
}
}
4.实现应用程序的主方法,创建包含main()
方法的Main
类:
public class Main {
public static void main(String[] args) {
5.首先,输出线程的最大值、最小值和默认优先级:
System.out.printf("Minimum Priority: %s\n",
Thread.MIN_PRIORITY);
System.out.printf("Normal Priority: %s\n",
Thread.NORM_PRIORITY);
System.out.printf("Maximun Priority: %s\n",
Thread.MAX_PRIORITY);
6.创建10个Thread
对象,分别用来执行10个Calculator
任务。再创建两个数组,用来保存Thread
对象及其State
对象。后续我们将用这些信息来查看线程的状态。这里将5个线程设置为最大优先级,另5个线程设置为最小优先级:
Thread threads[];
Thread.State status[];
threads = new Thread[10];
status = new Thread.State[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(new Calculator());
if ((i % 2) == 0) {
threads[i].setPriority(Thread.MAX_PRIORITY);
} else {
threads[i].setPriority(Thread.MIN_PRIORITY);
}
threads[i].setName("My Thread " + i);
}
7.接着将一些必要的信息保存到文件中,因此需要创建try-with-resources
语句来管理文件。在这个代码块中,先将线程启动前的状态写入文件,然后启动线程:
try (FileWriter file = new FileWriter(".\\data\\log.txt");
PrintWriter pw = new PrintWriter(file);) {
for (int i = 0; i < 10; i++) {
pw.println("Main : Status of Thread " + i + " : " +
threads[i].getState());
status[i] = threads[i].getState();
}
for (int i = 0; i < 10; i++) {
threads[i].start();
}
8.等待线程运行结束。在1.6节中,我们将用join()
方法来等待线程结束。本案例中,由于我们需要记录线程运行过程中状态的转变,因此不能使用join()
方法来等待线程结束,而应使用如下代码:
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);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
9.在上述代码中,我们通过调用writeThreadInfo()
方法来将线程信息记录到文件中。代码如下:
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");
}
10.运行程序,然后观察不同的线程是如何同时运行的。
下图是程序在控制台的输出,从中可以看到,线程正在并行处理各自的工作。
从下面的屏幕截图中可以看到线程是如何创建的,拥有高优先级的偶数编号线程比低优先级的奇数编号线程优先执行。该截图来自记录线程状态的log.txt
文件。
每个Java应用程序都至少有一个执行线程。在程序启动时,JVM会自动创建执行线程运行程序的main()
方法。
当调用Thread
对象的start()
方法时,JVM才会创建一个执行线程。也就是说,每个Thread
对象的start()
方法被调用时,才会创建开始执行的线程。
Thread
类的属性存储了线程所有的信息。操作系统调度执行器根据线程的优先级,在某个时刻选择一个线程使用CPU,并且根据线程的具体情况来实现线程的状态。
如果没有指定线程的名字,那么JVM会自动按照Thread
-XX
格式为线程命名,其中XX
是一个数字。线程的ID和状态是不可修改的,事实上,Thread
类也没有实现setId()
和setStatus()
方法,因为它们会引入对ID和状态的修改。
一个Java程序将在所有线程完成[1]后结束。初始线程(执行main()
方法的线程)完成,其他线程仍会继续执行直到完成。如果一个线程调用System.exit()命
令去结束程序,那么所有线程将会终止各自的运行。
创建一个Thread
对象并不意味着会创建一个新的执行线程。同样,调用实现Runnable
接口类的run()
方法也不会创建新的执行线程。只有调用了start()
方法,一个新的执行线程才会真正创建。
正如本节开头所说,还有另一种创建执行线程的方法——实现一个继承Thread
的类,并重写其run()
方法,创建该类的对象后,调用start()
方法即可创建执行线程。
可以使用Thread
类的静态方法currentThread()
来获取当前运行线程的Thread
对象。
调用setPriority()
方法时,需要对其抛出的IllegalArgumentException
异常进行处理,以防传入的优先级不在合法范围内(1和10之间)。
一个多线程Java程序,只有当其全部线程执行结束时(更具体地说,是所有非守护线程结束或者某个线程调用System.exit()
方法的时候),才会结束运行。有时,为了终止程序或者取消一个线程对象所执行的任务,我们需要终止一个线程。
Java使用一种中断机制来向线程表明想要终止它。这个中断机制依靠线程对象来检查是否需要中断,同时线程对象可以决定是否响应中断请求。当然,一个线程对象也可以忽略中断请求继续执行。
本节将开发一个应用程序,它的作用是在线程创建5s后,使用中断机制强制结束线程。
本案例是用Eclipse IDE 来实现的。如果开发者使用Eclipse 或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤来完成本案例。
1.创建一个名为PrimeGenerator
的类,并继承Thread
类:
public class PrimeGenerator extends Thread{
2.重写run()
方法——该方法包含一个无限while
循环。在循环中,处理从1开始的连续数字。如果是奇数,那么将其输出到控制台:
@Override
public void run() {
long number=1L;
while (true) {
if (isPrime(number)) {
System.out.printf("Number %d is Prime\n",number);
}
3.每处理完一个数字,通过isInterrupted()
方法来判断当前线程是否已被中断。如果该方法返回true
,那么表明当前线程已被中断。在这种情况下,在控制台上打印一条信息并终止线程:
if (isInterrupted()) {
System.out.printf("The Prime Generator has been Interrupted");
return;
}
number++;
}
}
4.实现isPrime()
方法。详细代码参见1.2节。
5.现在,开始实现应用程序的主类,创建包含main()方
法的Main
类:
public class Main {
public static void main(String[] args) {
6.创建PrimeGenerator
类的对象,并启动它:
Thread task=new PrimeGenerator();
task.start();
7.在主线程中等待5s后,中断PrimeGenerator
线程:
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
task.interrupt();
8.输出中断线程的状态。这段代码的输出结果取决于它是在线程结束前还是线程结束后运行的:
System.out.printf("Main: Status of the Thread: %s\n",task.getState());
System.out.printf("Main: isInterrupted: %s\n",task.isInterrupted());
System.out.printf("Main: isAlive: %s\n", task.isAlive());
}
9.运行案例并查看结果。
下面是以上案例运行结果的截图。从图中可以看到,PrimeGenerator
线程在检测到自己被中断后,输出信息并结束了运行。
Thread
类有一个用来保存线程是否已被中断的状态属性,其属性值为boolean
类型,默认值为false
。当调用一个线程对象的interrupt()
方法时,该状态属性将修改为true
。而方法isInterrupted()
仅返回该状态属性的值。
在main()
方法中,输出了中断线程的一些状态信息。在本案例中,虽然在这些代码之前调用了线程的中断,但是在执行这些代码时,任务线程并未执行到中断判断和处理过程,因此,此时输出的线程状态为RUNNABLE
,方法isInterrupted()
的结果为true
,当然方法isAlive()
的结果也为true
。如果这些代码执行是在Thread
中断完成之后[可以制造机会,如通过在main
调用sleep()
使得主线程休眠1s,使得task
线程完成中断,那么isInterrupted()
和isAlive()
的结果将为false
。
在Thread
类中,还有一个静态方法interrupted()
,也能用来检测当前线程是否已被中断。
注意:
isInterrupted()
方法和interrupted()
方法之间有一个重要的不同点:isInterrupted()
方法不会修改线程的是否中断属性,而interrupted()
方法会将中断属性设置为false
。
正如前文所说,线程对象可以忽略中断,但这并不是被预期的行为。
前面介绍了中断一个线程的方法,以及对线程中断必须要做的处理。尽管之前的案例展示了如何中断一个简单线程,但是当一个线程有划分成多个方法的复杂算法,或者有递归调用时,我们需要更好的机制来控制中断。为此,Java提供了InterruptedException
异常,可以在检测到线程中断后抛出该异常,并在run()
方法中捕获它。
本节将会实现一个在指定文件夹及其子文件夹下查找文件的线程,并展示如何使用InterruptedException
异常来控制线程的中断。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的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()
方法。首先检测initPath
是否为一个文件夹,如果是,则调用directoryProcess()
方法。该方法会抛出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.实现fileProcess()
方法。该方法将对比文件名是否与所要搜索的文件名相同,如果相同,则向控制台输出信息。完成对比后,该方法会判断线程是否已被中断,在本案例中,如果发生线程中断,则抛出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
对象,然后用Thread
对象启动线程来执行该任务。本案例采用的是Windows路径。在其他操作系统(如Linux或者iOS)下,需要将路径修改为对应系统上一个存在的文件的路径:
FileSearch searcher = new FileSearch("C:\\Windows",
"explorer.exe");
Thread thread=new Thread(searcher);
thread.start();
8.等待10s后中断线程:
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
9.运行案例并查看结果。
案例运行结果如下图所示。可以看到,FileSearch
对象在检测到自己被中断时,结束了执行。
本案例使用Java异常来控制线程的中断。在执行案例时,程序通过递归检查文件夹中是否包含指定文件。例如,如果想要进入\b\c\d
目录,则程序需要3次递归调用directoryProcess()
方法。无论有多少次递归调用,只要它检测到中断,就会立即抛出InterruptedException
异常,返回到run()
方法中继续执行。
Java的一些并发API也会抛出InterruptedException
异常,如sleep()
方法。正如在本案例中,已经休眠的线程被中断(使用interrupted()
方法),也抛出了该异常。
有时,可能需要在指定的时间段暂停一个线程的执行。例如,一个程序中的某个线程需要每分钟检测一次传感器的状态,其余时间保持空闲。在空闲时间段,线程并不使用任何计算机资源。在空闲时间段之后,该线程由执行调度器选中,继续执行。可以使用Thread
类的sleep()
方法来达到该目的。该方法接收一个long
类型的参数——该参数是线程将要暂停的时长。在暂停时间过后,JVM会重新给该线程分配CPU时间,该线程将继续执行,直到下一个sleep()
指令。
还有一种途径,可以使用TimeUnit
枚举元素的sleep()
方法。该方法调用当前Thread
类的sleep()
方法,使当前线程进入休眠。但是,其接收的时长参数是以其代表的时间为单位的,其内部实现会在调用线程的方法时自动将该时长转化为毫秒单位的值。
本节将实现一个应用,即使用sleep()
方法来打印每一秒的时间。
本案例是用Eclipse IDE 实现的。如果开发者使用Eclipse或者其他IDE(如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.创建一个名为ConsoleClock
的类,并实现Runnable
接口:
public class ConsoleClock implements Runnable {
2.实现run()
方法:
@Override
public void run(){
3.实现一个迭代10次的循环。在每一次迭代中,创建一个Date
对象,并将其输出至控制台,然后,调用TimeUnit
类SECONDS
属性的sleep()
方法,使当前线程的执行暂停1s。sleep()
方法会抛出InterruptedException
异常,因此程序中需要包含处理代码。中断异常的catch部分释放线程使用的资源,这是良好的编程习惯:
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.在实现线程任务后,开始实现应用程序入口,创建包含main()
方法的Main
类:
public class Main {
public static void main(String[] args) {
5.创建一个ConsoleClock
类对象和执行该对象的线程,然后开始执行线程:
ConsoleClock clock = new ConsoleClock();
Thread thread=new Thread(clock);
thread.start();
6.调用TimeUnit
类SECONDS
属性的sleep()
方法,使得主线程休眠5s:
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
};
7.中断ConsoleClock
线程:
thread.interrupt();
8.运行案例并查看结果。
运行案例后,可以看到程序每秒打印的Date
对象以及ConsoleClock
线程中断的信息。
当调用sleep()
方法时,线程释放CPU资源,停止执行指定的时间。在这段时间里,线程并不消耗CPU时间,因此CPU可以执行其他任务。
当线程在休眠中发生中断时,该方法会立即抛出一个InterruptedException
异常,而不会等到休眠时间结束。
在Java并发API中,还有一个方法可以使线程释放CPU资源,即yield()
方法。该方法告知JVM当前线程可以为其他任务放弃CPU资源。JVM并不保证一定会响应该请求。通常只在调试中使用该方法。
在一些场景中,我们必须等待某个线程执行完毕(即run()
方法执行结束)。例如,一个程序在必要的资源初始化完毕后,才能开始后续的执行工作。可以将初始化任务作为单独的线程执行,待其结束后再执行其余线程。
使用Thread
类的join()
方法可以实现这个目的。当调用一个线程对象的join()
方法时,发起调用的线程将会暂停,直到线程对象执行结束。
本节将介绍如何在案例初始化过程中使用join()
方法。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse 或者其他IDE(如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.创建一个名为DataSourcesLoader
的类,并实现Runnable
接口:
public class DataSourcesLoader implements Runnable {
2.实现run()
方法。向控制台输出执行开始的信息,休眠4s,然后再输出一条执行结束的信息:
@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()
方法,它与DataSourcesLoader
的run()
方法基本一致,但它的休眠时间为6s。
4.实现应用程序入口,创建包含main()
方法的Main
类:
public class Main {
public static void main(String[] args) {
5.创建一个DataSourcesLoader
类的对象,并创建一个Thread
对象来执行该任务:
DataSourcesLoader dsLoader = new DataSourcesLoader();
Thread thread1 = new Thread(dsLoader,"DataSourceThread");
6.创建一个NetworkConnectionsLoader
类的对象,并创建一个Thread
对象来执行该任务:
NetworkConnectionsLoader ncLoader = new NetworkConnectionsLoader();
Thread thread2 = new Thread(ncLoader,"NetworkConnectionLoader");
7.调用两个线程对象的start()
方法:
thread1.start();
thread2.start();
8.用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()
方法的重载版本,不是无限期地等待被调用的线程对象执行完毕,而是最多等待参数中指定的毫秒数。例如,如果在thread1
中调用了thread2.join(1000)
,那么,thread1
线程暂停执行,直到遇到以下两个条件之一。
thread2
结束执行。只要上述两个条件之一为true
,join()
方法就会返回。通过线程状态,可以得知join()
方法是因为执行结束,还是因为指定时间已到而返回。
第二个join()
方法的重载版本与第一个类似,不同之处在于该方法接收毫秒和纳秒作为参数[2]。
Java有一种名为守护(daemon)线程的特殊线程。当程序中仅剩守护线程还在运行时,JVM会先结束这些线程然后结束程序。
正是因为这些特性,守护线程通常作为服务提供者,为同一应用内的普通(也称为用户)线程提供服务。守护线程通常包含一个无限循环,来等待一个线程的服务请求或者线程任务。守护线程的典型案例就是Java的垃圾回收器。
本节将通过案例来介绍如何使用守护线程。该案例有两个线程:一个是用户线程,用于向队列写入事件;另一个是守护线程,用于清理队列中超过10s的事件。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.创建一个名为Event
的类。该类存储了程序中所使用的事件信息。声明两个私有属性:一个是java.util.Date
类型的属性date,一个String
类型的属性event。生成读写这些属性的方法。
2.创建一个名为WriterTask
的类,并实现Runnable
接口:
public class WriterTask implements Runnable {
3.声明一个用于存储事件的队列,并在类构造器中完成队列初始化:
private Deque<Event> deque;
public WriterTask (Deque<Event> deque){
this.deque=deque;
}
4.实现该任务类的run()
方法。该方法包含100次的迭代循环。在每一次迭代中,我们创建一个新的事件并将其存储到队列当中,然后休眠1s:
@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.声明一个用于存储事件的队列,并在类构造器中完成队列初始化:
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()
方法。该方法获取队列中的最后一个事件,如果该事件已创建超过10s,则从队列中删除该事件并检查下一个事件。如果删除了一个事件,则输出该事件信息及队列大小,便于观察执行过程:
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 deque=new ConcurrentLinkedDeque();
11.根据`JVM`的可用处理器个数创建对应数量的线程,执行`WriterTask`任务,接着创建一个`CleanerTask`线程任务:
WriterTask writer=new WriterTask(deque);
for (int i=0; i< Runtime.getRuntime().availableProcessors();
i++){
Thread thread=new Thread(writer);
thread.start();
}
CleanerTask cleaner=new CleanerTask(deque);
cleaner.start();
12.运行案例并查看结果。
分析程序的执行输出不难发现队列内的事件数不断增长,直到40
,然后在40
左右浮动,直到程序结束。当然,这个大小取决于计算机的内核数。本次执行运行在四核处理器上,因此启动了4个WriterTask
任务。
程序一开始便启动了4个WriterTask
线程,每个线程向队列写入一个事件并休眠1s。在第一个10
s后,队列中便存储了40
个事件。在这10
s中,CleanerTask
线程在4个WriterTask
线程休眠时开始执行;由于所有事件均在10s内产生,因此,不会从队列中删除任何事件。在接下来的执行过程中,每一秒,CleanerTask
会删除4个事件,WriterTask
写入另外4个事件。因此,队列中的事件个数总在40
左右。正如前文所说,案例执行取决于JVM可用内核数,通常而言,这个数等于CPU的内核数。
当然,你也可以修改WriterTask
线程休眠时间,当其休眠时间足够小时,CleanerTask
只能获得更少的CPU时间,从而导致CleanerTask
无法及时删除队列事件,队列中的事件数量会不断增长。
setDaemon()
方法只能在start()
方法之前调用,一旦线程开始执行,其daemon
状态便不可修改。此时调用setDaemon()
方法,将抛出IllegalThreadStateException
异常。
通过isDaemon()
方法可以检查线程是一个守护线程(此时方法返回true
)还是一个非守护线程(此时方法返回为false
)。
在任何编程语言中,提供对应用中错误情景的管理机制都是非常重要的。和其他现代编程语言一样,Java编程语言提供了基于异常的错误管理机制。当发生错误情况时,Java将抛出异常类。除了已有的异常类,还可以自定义异常类,以便管理类中产生的错误。
Java也提供了捕获和处理异常的机制。有一些异常必须被捕获或者通过方法上的throws
声明再次抛出,这类异常被称为检查异常。而有一些异常不需要方法上的声明或捕获,这类异常被称为非检查异常。
throws
语句中声明,或者在方法内部捕获,例如IOException
或ClassNotFoundException
。NumberFormatException
。如果在一个线程对象的run()
方法内抛出检查异常,则必须对其进行捕获处理,因为run()
方法不接受throws
语句。如果一个非检查异常在一个线程对象的run()
方法内被抛出,则会默认将异常栈信息打印到控制台,并退出程序。
幸运的是,Java提供了用于捕获和处理线程对象中抛出的非检查异常机制,以避免程序的结束。
本节将在案例中使用这种机制。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.首先,实现一个处理非检查异常的类。该类必须实现UncaughtExceptionHandler
接口,并实现接口中声明的uncaughtException()
方法。该接口在Thread
类的内部定义,在本案例中,我们将处理类命名为ExceptionHandler
,并创建一个用于输出异常和抛出线程信息的方法,具体代码如下所示:
public class ExceptionHandler implements UncaughtExceptionHandler {
@Override
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
对象执行该任务。通过setUncaughtException- Handler()
方法为线程设置非检查异常处理器,并启动执行线程:
Task task=new Task();
Thread thread=new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
5.运行案例并查看结果。
案例运行结果如下图所示。异常被抛出后,异常处理器会将其捕获,然后输出该异常及抛出线程的信息。这些信息均打印到控制台:
当线程中抛出一个异常且未捕获(必须是非检查异常)时,JVM会检查是否通过相关方法为该线程配置了未捕获异常处理器(UncaughtExceptionHandler)。如果有,那么JVM将调用线程对象上相应的方法,并传递抛出的异常作为参数。
如果线程对象没有配置未捕获异常处理器,则JVM会在控制台中打印出异常信息栈,然后结束异常抛出线程的执行。
Thread
类中还定义了另一个用于处理未捕获异常的方法,即静态方法setDefault- UncaughtExceptionHandler()
。该方法可以为应用中所有线程对象设置默认的未捕获异常处理器。
当线程中抛出未捕获异常时,JVM会为该异常依次查找3个可能的处理器。
首先,如本节所介绍的,JVM会查找线程对象的未捕获异常处理器。如果该处理器不存在,则JVM将查找在1.10节中介绍的线程组的未捕获异常处理器。如果该处理器仍然不存在,则JVM将会查找默认的异常处理器。
当然,如果上述异常处理器都不存在,那么JVM会在控制台中打印异常信息栈,然后结束异常抛出线程的执行。
对于并发应用程序来说,数据共享是非常重要的一个方面。对于继承Thread
类或者实现Runnable
接口的类来说,在两个或多个线程间共享数据是极其重要的。
如果创建了实现Runnable
接口的类对象,并用多个线程对象来执行该任务对象,那么所有线程都能够共享该任务对象的属性。这意味着,如果在一个线程中更改了该任务对象的属性,那么其他线程也会受到相应影响。
有时希望多线程同时执行一个任务的实例时,能够独享一个变量。为此Java并发API中的线程本地变量(thread-local variable)提供了良好的支持。只要线程存活,线程本地变量就会一直存在,因此该机制也存在一些缺点。比如,在一些线程复用的场景下,这就可能产生问题。
本节将会实现两个案例:第一个案例实现第一段中描述的问题;第二个案例使用线程本地变量来解决这个问题。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse 或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.首先实现一个程序来展现之前描述的问题。创建一个名为UnsafeTask
的类,实现Runnable
接口,并声明一个私有的java.util.Date
型属性:
public class UnsafeTask implements Runnable{
private Date startDate;
2.实现该类的run()
方法。该方法将初始化并打印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
类。在main()
方法中,创建一个UnsafeTask
实例,并用10
个线程对象执行该任务,每隔2s启动一个执行线程:
public class Main {
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.程序的执行结果如下图所示。可以看出,虽然每个线程都有不同的启动时间,但在线程结束时,这些时间值又发生了变化,因为线程进行了错误的写,例如,检查ID号为13的线程[3]:
5.正如之前所述,可以采用线程本地变量(thread-local variable)机制来解决这个问题。
6.创建一个名为SafeTask
的类,并实现Runnable
接口:
public class SafeTask implements Runnable {
7.声明一个ThreadLocal<Date>
类的实例,该实例的隐式实现包含返回当前时间的initialValue()
方法:
private static ThreadLocal<Date> startDate=new
ThreadLocal<Date>(){
protected Date initialValue(){
return new Date();
}
};
8.实现run()
方法。虽然该方法的作用与UnsafeTask
的run()
方法相同,但是,它们访问startDate
属性的方式不同,这里使用startDate
对象的get()
方法:
@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.该案例的Main
类与之前的案例基本相同,区别在于Runnable
类的名字不同。
10.运行案例并查看结果。
案例运行结果如下图所示。10个Thread
对象都拥有其独立的startDate
属性值:
线程本地变量机制为每个使用该属性的线程保存了独立的属性值。可以用get()
和set()
方法来分别读写该属性值。第一次访问线程本地变量时,若与该线程对象关联的属性值不存在,则将会触发initialValue()
方法,它会为该属性赋值并返回初始值。
ThreadLocal
类提供了remove()
方法,该方法用于删除调用线程的线程本地变量值。
Java并发API中提供的InheritableThreadLocal
类能够实现从线程的创建线程上继承本地变量的功能。如果线程A有一个线程本地变量,它又创建了线程B,那么线程B也拥有了与A相同的线程本地变量。我们可以重写childValue()
方法,该方法为新创建的子线程初始化了本地变量。该方法会接收父线程的线程本地变量值作为参数。
Java的并发API提供了一个有意思的功能,即对线程进行分组。这让开发者能够将线程组视为一个整体单元,它提供了对组内线程对象的访问支持。这意味着,当一组线程执行相同任务时,可以同时控制这些线程。当然,也可以通过一个信号中断所有组内线程。
Java提供了ThreadGroup
类来处理线程组。一个ThreadGroup
对象可以由一组线程对象或者其他ThreadGroup
对象组成,形成一个线程的树形结构。
1.4节介绍了处理线程对象内部抛出未捕获异常的通用方法。1.8节采用处理器(handler)来处理线程中的未捕获异常。对于线程组抛出的异常,也可以采用相同的机制进行处理。
在本案例当中,我们将介绍使用ThreadGroup
对象的方法,以及如何实现和配置处理器来对线程组中抛出的未捕获异常进行处理。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤完成本案例。
1.创建一个名为MyThreadGroup
的类,并继承ThreadGroup
类进行扩展。ThreadGroup
类没有无参构造器,因此必须声明一个拥有一个参数的构造器。为了处理线程组抛出的异常,还需要重写uncaughtException()
方法:
public class MyThreadGroup extends ThreadGroup {
public MyThreadGroup(String name) {
super(name);
}
2.重写uncaughtException()
方法。ThreadGroup
类中的任意一个线程抛出异常,都将调用该方法。在本案例中,该方法将在控制台上输出异常和线程的信息。同时需要注意,该方法会中断线程组中的其余线程:
@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除以随机生成的整数,直到生成的随机数为零,这时会抛出异常:
@Override
public void run() {
int result;
Random random=new Random(Thread.currentThread().getId());
while (true) {
result=1000/((int)(random.nextDouble()*1000000000));
if (Thread.currentThread().isInterrupted()) {
System.out.printf("%d : Interrupted\n",
Thread.currentThread().getId());
return;
}
}
}
5.实现应用程序入口,创建包含main()
方法的Main
类:
public class Main {
public static void main(String[] args) {
6.计算将要启动的线程数。使用Runtime
类的availableProcessors()
方法[使用Runtime
类的静态方法getRuntime()
得到当前应用的Runtime
对象],可以得到JVM中可用的处理器数,它通常与运行该应用的计算机内核数一致:
int numberOfThreads = 2 * Runtime.getRuntime()
.availableProcessors();
7.创建MyThreadGroup
类的对象:
MyThreadGroup threadGroup=new MyThreadGroup("MyThreadGroup");
8.创建Task
类的对象:
Task task=new Task();
9.创建之前计算得出的数量的Thread
对象,执行task
并启动:
for (int i = 0; i < numberOfThreads; i++) {
Thread t = new Thread(threadGroup, task);
t.start();
}
10.向控制台输出ThreadGroup
的信息:
System.out.printf("Number of Threads: %d\n",
threadGroup.activeCount());
System.out.printf("Information about the Thread Group\n");
threadGroup.list();
11.输出线程组中各线程状态:
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.运行案例并查看结果。
从下图可以看到,ThreadGroup
类的list()
方法的输出,以及打印的每个Thread对象的状态。
ThreadGroup
类存储了关联的线程对象及关联的其他线程组对象,因此它可以对其组成员进行信息访问(如状态信息)和控制(如中断控制)。
可以观察到,线程组中任意线程抛出异常后,触发了其uncaughtException()
方法。该方法紧接着中断了其余线程:
当一个Thread
对象中抛出未捕获异常时,JVM会为该异常依次查找3个可能的处理器。
首先,JVM会查找线程对象的未捕获异常处理器。如果该处理器不存在,则JVM将查找在1.8节中介绍的线程组的未捕获异常处理器。如果该处理器仍然不存在,则JVM将会查找默认的异常处理器。
当然,如果上述异常处理器都不存在,那么JVM会在控制台中打印异常信息栈,然后结束异常抛出线程的执行。
工厂模式是面向对象编程世界中最常用的设计模式之一。这是一种创造性模式,其实例对象的目标是创建一个或者多个类的实例。因此,如果想要创建这些类的实例,那么可以采用工厂对象来替代新的操作符。
通过使用工厂对象,我们可以集中处理对象的创建过程,这样做有以下优点。
Java提供了一个接口(ThreadFactory
接口)来实现线程对象工厂。Java并发API的一些高级实用程序也是使用线程工厂来创建线程的。
本节介绍通过实现一个ThreadFactory
接口的类来创建具有个性化名称的线程对象,并保存线程对象创建过程的统计信息。
本案例是用Eclipse IDE实现的。如果开发者使用Eclipse或者其他IDE(例如NetBeans),则应打开它并创建一个新的Java项目。
根据以下步骤来完成本案例。
1.创建一个名为MyThreadFactory
的类,并实现ThreadFactory
接口:
public class MyThreadFactory implements ThreadFactory {
2.声明3个属性:一个整型变量counter
用于存储线程对象的创建数;一个字符串变量name
,用于存储每个新建线程的基础名称;一个字符串列表stats
,用于存储线程对象创建过程的统计信息。在构造器中初始化这些属性:
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
接口,并为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.实现getStats()
方法。该方法返回一个String
对象——其包含创建的所有线程对象的统计数据:
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
接口。在本案例中,这些任务将休眠1s,除此之外,不会做任何事情:
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 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
对象作为参数,并返回一个Thread
对象。实现一个ThreadFactory
接口时,必须覆盖newThread()
方法。实现最基本的ThreadFactory
仅需一行代码:
return newThread(r);
当然,也可以通过增加一些变量来增强该类,方法如下所示。
Thread
类来创建自己的线程类。除此之外,你还可以添加任何需要的信息到前面的列表中。使用工厂设计模式是一种很好的编程习惯,但如果实现一个ThreadFactory
接口来集中创建线程,则必须查看代码,以确保所有线程都由同一工厂创建。
[1] 准确来说,是所有非守护线程完成。——译者注
[2] 毫秒+纳秒的形式使等待时长更精确。——译者注
[3] 可以看到属性值前后不一致。——译者注