java多线程编程核心技术


Java 核心技术系列 Java 多线程编程核心技术 高洪岩 著 图书在版编目(CIP)数据 Java 多线程编程核心技术 / 高洪岩著 . —北京:机械工业出版社,2015.6 (Java 核心技术系列) ISBN 978-7-111-50206-7 I. J… II. 高… III. JAVA 语言-程序设计 IV. TP312 中国版本图书馆 CIP 数据核字(2015)第 098874 号 Java 多线程编程核心技术 出版发行:机械工业出版社(北京市西城区百万庄大街 22 号 邮政编码:100037) 责任编辑:高婧雅 责任校对:董纪丽 印  刷: 版  次:2015 年 6 月第 1 版第 1 次印刷 开  本:186mm×240mm 1/16 印  张:19.75 书  号:ISBN 978-7-111-50206-7 定  价:69.00 元 凡购本书,如有缺页、倒页、脱页,由本社发行部调换 客服热线:(010)88379426 88361066 投稿热线:(010)88379604 购书热线:(010)68326294 88379649 68995259 读者信箱:hzit@hzbook.com 版权所有 • 侵权必究 封底无防伪标均为盗版 本书法律顾问:北京大成律师事务所 韩光 / 邹晓东 Preface 前  言 为什么要写这本书 早在几年前笔者就曾想过整理一份与 Java 多线程有关的稿件,因为市面上所有的 Java 书籍都是以一章或两章的篇幅介绍多线程技术,并没有完整地覆盖该技术的知识点,但可 惜,苦于当时的时间及精力有限,一直没有达成所愿。 也许是注定的安排,我目前所在的单位是集技术与教育为一体的软件类企业。我在工作 中发现很多学员在学习完 JavaSE/JavaEE 之后想对更深入的技术进行探索,比如在对大数据、 分布式、高并发类的专题进行攻克时,立即遇到针对 java.lang 包中 Thread 类的学习,但 Thread 类的学习并不像 JDBC 那样简单,学习多线程会遇到太多的问题、弯路以及我们所谓 的“坑”,为了带领学员在技术层面上进行更高的追求,我将多线程的技术点以教案的方式 进行整理,在课堂上与同学们一起学习、交流,同学们反响也非常热烈。此至,若干年前的 心愿终于了却,学员们也很期待这本书能出版发行,因为这样他们就有了真正的纸质参考资 料,其他爱好 Java 多线程的朋友们也在期盼本书的出版。本书能促进他们相互交流与学习, 这就是我最大的心愿。 本书秉承大道至简的主导思想,只介绍 Java 多线程开发中最值得关注的内容,希望能抛 砖引玉,以个人的一些想法和见解,为读者拓展出更深入、更全面的思路。 本书特色 在本书写作的过程中,我尽量减少“啰嗦”的文字语言,全部用案例来讲解技术点的实 现,使读者看到代码及运行结果后就可以知道此项目要解决的是什么问题,类似于网络中的 博客风格,可让读者用最短的时间学完相关知识点,明白这些知识点是如何应用的,以及在 IV 使用时要避免什么。本书就像“瑞士军刀”一样,精短小,但却非常锋利,可帮读者快速学 习知识并解决问题。 读者对象 本书适合所有 Java 程序员阅读,尤其适合以下读者: ‰‰ Java 多线程开发者 ‰‰ Java 并发开发者 ‰‰ 系统架构师 ‰‰ 大数据开发者 ‰‰ 其他对多线程技术感兴趣的人员 如何阅读本书 在整理本书时,我一直本着实用、易懂的原则,最终整理出 7 章: 第 1 章讲解了 Java 多线程的基础,包括 Thread 类的核心 API 的使用。 第 2 章讲解了在多线程中对并发访问的控制,主要就是 synchronized 的使用,由于此关 键字在使用上非常灵活,所以书中用了很多案例来介绍此关键字的使用,为读者学习同步相 关内容打好坚实的基础。 第 3 章介绍线程并不是孤独的,它们之间要通信,要交互。本章主要介绍wait()、 notifyAll() 和 notify() 方法的使用,使线程间能互相通信,合作完成任务。本章还介绍了 ThreadLocal 类的使用。学习完本章,读者就能在 Thread 多线程中进行数据的传递了。 第 4 章讲解了 synchronized 关键字,它使用起来比较麻烦,所以在 Java 5 中提供了 Lock 对象,以求能更好地实现并发访问时的同步处理,包括读写锁等相关技术点。 第 5 章讲解了 Timer 定时器类,其内部实现就是使用的多线程技术。定时器的计划任 务执行是很重要的技术点,包括在 Android 开发时都会有深入的使用,所以会为读者详细 讲解。 第 6 章讲解的单例模式虽然很简单,但如果遇到多线程将会变得非常麻烦,如何在多线 程中解决这么棘手的问题呢?本章将全面介绍解决方案。 第 7 章,在整理稿件的过程中肯定会出现一些技术知识点的空缺,前面被遗漏的技术案 例将在本章进行补充,以帮助读者形成完整的多线程的知识体系。编写本章的目的就是尽量 使本书不存在技术空白点。 V 勘误和支持 由于我的水平有限,编写时间仓促,书中难免会出现一些错误或者不准确的地方,恳请 读者批评指正,让我与大家一起,在技术之路上互勉共进。我的邮箱是 279377921@qq.com, 期待能够得到你们的真挚反馈。本书的源代码可以在华章网站(www.hzbook.com)下载。 致谢 感谢所在单位领导的支持与厚爱,使我在技术道路上更有信心。 感谢机械工业出版社华章公司的高婧雅和杨福川,因为有了你们的鼓励、帮助和引导, 我才能顺利完成本书。 高洪岩 目  录 Contents 前 言 第 1 章 Java 多线程技能 1 1.1 进程和多线程的概念及线程的 优点 1 1.2 使用多线程 3 1.2.1 继承 Thread 类 4 1.2.2 实现 Runnable 接口 8 1.2.3 实例变量与线程安全 9 1.2.4 留意 i-- 与 System.out.println() 的异常 14 1.3 currentThread() 方法 16 1.4 isAlive() 方法 18 1.5 sleep() 方法 20 1.6 getId() 方法 22 1.7 停止线程 23 1.7.1 停止不了的线程 23 1.7.2 判断线程是否是停止状态 24 1.7.3 能停止的线程——异常法 27 1.7.4 在沉睡中停止 30 1.7.5 能停止的线程——暴力停止 32 1.7.6 方法 stop() 与 java.lang. ThreadDeath 异常 33 1.7.7 释放锁的不良后果 34 1.7.8 使用 return 停止线程 35 1.8 暂停线程 36 1.8.1 suspend 与 resume 方法的 使用 36 1.8.2 suspend 与 resume 方法的缺 点——独占 38 1.8.3 suspend 与 resume 方法的 缺点——不同步 40 1.9 yield 方法 42 1.10 线程的优先级 43 1.10.1 线程优先级的继承特性 43 1.10.2 优先级具有规则性 44 1.10.3 优先级具有随机性 47 1.10.4 看谁运行得快 49 1.11 守护线程 50 1.12 本章小结 51 第 2 章 对象及变量的并发访问 52 2.1 synchronized 同步方法 52 VII 2.1.1 方法内的变量为线程安全 53 2.1.2 实例变量非线程安全 54 2.1.3 多个对象多个锁 57 2.1.4 synchronized 方法与锁对象 59 2.1.5 脏读 63 2.1.6 synchronized 锁重入 65 2.1.7 出现异常,锁自动释放 68 2.1.8 同步不具有继承性 69 2.2 synchronized 同步语句块 71 2.2.1 synchronized 方法的弊端 72 2.2.2 synchronized 同步代码块的 使用 74 2.2.3 用同步代码块解决同步方法的 弊端 76 2.2.4 一半异步,一半同步 76 2.2.5 synchronized 代码块间的 同步性 78 2.2.6 验证同步 synchronized(this) 代码块是锁定当前对象的 80 2.2.7 将任意对象作为对象监视器 82 2.2.8 细化验证 3 个结论 91 2.2.9 静态同步 synchronized 方法与 synchronized(class) 代码块 96 2.2.10 数据类型 String 的常量池 特性 102 2.2.11 同步 synchronized 方法无限 等待与解决 105 2.2.12 多线程的死锁 107 2.2.13 内置类与静态内置类 109 2.2.14 内置类与同步:实验 1 111 2.2.15 内置类与同步:实验 2 113 2.2.16 锁对象的改变 114 2.3 volatile 关键字 118 2.3.1 关键字 volatile 与死循环 118 2.3.2 解决同步死循环 119 2.3.3 解决异步死循环 120 2.3.4 volatile 非原子的特性 124 2.3.5 使用原子类进行 i++ 操作 126 2.3.6 原子类也并不完全安全 127 2.3.7 synchronized 代码块有 volatile 同步的功能 130 2.4 本章总结 132 第 3 章 线程间通信 133 3.1 等待 / 通知机制 133 3.1.1 不使用等待 / 通知机制实现 线程间通信 133 3.1.2 什么是等待 / 通知机制 135 3.1.3 等待 / 通知机制的实现 136 3.1.4 方法 wait() 锁释放与 notify() 锁不释放 143 3.1.5 当 interrupt 方法遇到 wait 方法 146 3.1.6 只通知一个线程 148 3.1.7 唤醒所有线程 150 3.1.8 方法 wait(long) 的使用 150 3.1.9 通知过早 152 3.1.10 等待 wait 的条件发生 变化 155 3.1.11 生产者 / 消费者模式实现 158 3.1.12 通过管道进行线程间通信: 字节流 171 VIII 3.1.13 通过管道进行线程间通信: 字符流 174 3.1.14 实战:等待 / 通知之交叉 备份 177 3.2 方法 join 的使用 179 3.2.1 学习方法 join 前的铺垫 179 3.2.2 用 join() 方法来解决 180 3.2.3 方法 join 与异常 181 3.2.4 方法 join(long) 的使用 183 3.2.5 方法 join(long) 与 sleep(long) 的区别 184 3.2.6 方法 join() 后面的代码提前 运行:出现意外 187 3.2.7 方法 join() 后面的代码提前 运行:解释意外 189 3.3 类 ThreadLocal 的使用 191 3.3.1 方法 get() 与 null 191 3.3.2 验证线程变量的隔离性 192 3.3.3 解决 get() 返回 null 问题 195 3.3.4 再次验证线程变量的 隔离性 195 3.4 类 InheritableThreadLocal 的 使用 197 3.4.1 值继承 197 3.4.2 值继承再修改 198 3.5 本章总结 199 第 4 章 Lock 的使用 200 4.1 使用 ReentrantLock 类 200 4.1.1 使用 ReentrantLock 实现同步:测试 1 200 4.1.2 使用 ReentrantLock 实现同步:测试 2 202 4.1.3 使用 Condition 实现等待 / 通知错误用法与解决 204 4.1.4 正确使用 Condition 实现等待 / 通知 207 4.1.5 使用多个 Condition 实现通知 部分线程:错误用法 208 4.1.6 使用多个 Condition 实现通知 部分线程:正确用法 210 4.1.7 实现生产者 / 消费者模式: 一对一交替打印 213 4.1.8 实现生产者 / 消费者模式: 多对多交替打印 214 4.1.9 公平锁与非公平锁 216 4.1.10 方法 getHoldCount()、 getQueueLength() 和 getWaitQueueLength() 的测试 219 4.1.11 方法 hasQueuedThread()、 hasQueuedThreads() 和 hasWaiters() 的测试 222 4.1.12 方法 isFair()、 isHeldByCurrentThread() 和 isLocked() 的测试 224 4.1.13 方法 lockInterruptibly()、 tryLock() 和 tryLock(long timeout,TimeUnit unit) 的测试 226 4.1.14 方法 awaitUninterruptibly() 的使用 230 IX 4.1.15 方法 awaitUntil() 的使用 232 4.1.16 使用 Condition 实现顺序 执行 234 4.2 使用 ReentrantReadWriteLock 类 236 4.2.1 类 ReentrantReadWriteLock 的使用:读读共享 236 4.2.2 类 ReentrantReadWriteLock 的使用:写写互斥 237 4.2.3 类 ReentrantReadWriteLock 的使用:读写互斥 238 4.2.4 类 ReentrantReadWriteLock 的使用:写读互斥 239 4.3 本章总结 240 第 5 章 定时器 Timer 241 5.1 定时器 Timer 的使用 241 5.1.1 方法 schedule(TimerTask task, Date time)的测试 241 5.1.2 方法 schedule(TimerTask task, Date firstTime, long period) 的测试 247 5.1.3 方法 schedule(TimerTask task, long delay) 的测试 252 5.1.4 方法 schedule(TimerTask task, long delay, long period) 的测试 253 5.1.5  方法 scheduleAtFixedRate (TimerTask task, Date firstTime, long period) 的测试 254 5.2 本章总结 261 第 6 章 单例模式与多线程 262 6.1 立即加载 /“饿汉模式” 262 6.2 延迟加载 /“懒汉模式” 263 6.3 使用静态内置类实现单例模式 271 6.4 序列化与反序列化的单例模式 实现 272 6.5 使用 static 代码块实现 单例模式 274 6.6 使用 enum 枚举数据类型实现 单例模式 275 6.7 完善使用 enum 枚举实现 单例模式 277 6.8 本章总结 278 第 7 章 拾遗增补 279 7.1 线程的状态 279 7.1.1 验证 NEW、RUNNABLE 和 TERMINATED 280 7.1.2 验证 TIMED_WAITING 281 7.1.3 验证 BLOCKED 282 7.1.4 验证 WAITING 284 7.2 线程组 285 7.2.1 线程对象关联线程组: 1 级关联 285 7.2.2 线程对象关联线程组: 多级关联 287 7.2.3 线程组自动归属特性 288 7.2.4 获取根线程组 288 7.2.5 线程组里加线程组 289 7.2.6 组内的线程批量停止 290 7.2.7 递归与非递归取得组内对象 290 X 7.3 使线程具有有序性 291 7.4 SimpleDateFormat 非线程安全 293 7.4.1 出现异常 293 7.4.2 解决异常方法 1 294 7.4.3 解决异常方法 2 295 7.5 线程中出现异常的处理 297 7.6 线程组内处理异常 299 7.7 线程异常处理的传递 301 7.8 本章总结 306 第 1 章 Java 多线程技能 作为本书的第 1 章,一定要引导读者快速进入 Java 多线程的学习,所以本章中主要介绍 Thread 类中的核心方法。Thread 类的核心方法较多,读者应该着重掌握如下关键技术点: ‰‰ 线程的启动 ‰‰ 如何使线程暂停 ‰‰ 如何使线程停止 ‰‰ 线程的优先级 ‰‰ 线程安全相关的问题 上面的 5 点也是本章学习的重点与思路,掌握这些内容是学习 Java 多线程的必经之路。 1.1 进程和多线程的概念及线程的优点 本节主要介绍在 Java 语言中使用多线程技术。但讲到多线程这个技术时不得不提及“进 程”这个概念,“百度百科”里对“进程”的解释如图 1-1 所示。 图 1-1 进程的解释 Chapter 1 2   Java 多线程编程核心技术 初看这段文字会觉得十分的抽象,难以理解,但如果你看到图 1-2 所示的内容,那么你 对进程还不能理解吗? 图 1-2 Windows 7 系统中的进程列表 难道可以将一个正在操作系统中运行的 exe 程序理解成一个“进程”吗?没错! 通过查看“ Windows 任务管理器”中的列表,完全可以将运行在内存中的 exe 文件理解 成进程,进程是受操作系统管理的基本运行单元。 那什么是线程呢?线程可以理解成是在进程中独立运行的子任务。比如,QQ.exe 运行时 就有很多的子任务在同时运行。再如,好友视频线程、下载文件线程、传输数据线程、发送表 情线程等,这些不同的任务或者说功能都可以同时运行,其中每一项任务完全可以理解成是 “线程”在工作,传文件、听音乐、发送图片表情等功能都有对应的线程在后台默默地运行。 这样做有什么优点呢?更具体来讲,使用多线程有什么优点呢?其实如果读者有使用 “多任务操作系统”的经验,比如 Windows 系列,那么它的方便性大家应该都有体会:使用 多任务操作系统 Windows 后,可以最大限度地利用 CPU 的空闲时间来处理其他的任务,比 如一边让操作系统处理正在由打印机打印的数据,一边使用 Word 编辑文档。而 CPU 在这些 任务之间不停地切换,由于切换的速度非常快,给使用者的感受就是这些任务似乎在同时运 第 1 章 Java 多线程技能   3 行。所以使用多线程技术后,可以在同一时间内运行更多不同种类的任务。 为了更加有效地理解多线程的优势,看一下如图 1-3 所示的单任务的模型图,理解一下 单任务的缺点。 在图 1-3 中,任务 1 和任务 2 是两个完全独立、互不相关的任务,任务 1 是在等待远程 服务器返回数据,以便进行后期的处理,这时 CPU 一直处于等待状态,一直在“空运行”。 如果任务 2 是在 10 秒之后被运行,虽然执行任务 2 用的时间非常短,仅仅是 1 秒,但也必 须在任务 1 运行结束后才可以运行任务 2。本程序是运行在单任务环境中,所以任务 2 有非 常长的等待时间,系统运行效率大幅降低。单任务的特点就是排队执行,也就是同步,就像 在 cmd 中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单 任务环境的缺点,即 CPU 利用率大幅降低。 而多任务的环境如图 1-4 所示。 㦯㹒1䇤㬒10㘌 㦯㹒2䇤㬒1㘌 ⭆ 㦯 㹒 ⭥ ⿘ ㈔ 㦯㹒1䇤㬒10㘌㦯㹒2䇤㬒1㘌 ⱁ㦯㹒⭥⿘㈔ 图 1-3 单任务运行环境 图 1-4 多任务运行环境 在图 1-4 中可以发现,CPU 完全可以在任务 1 和任务 2 之间来回切换,使任务 2 不必等 到 10 秒再运行,系统的运行效率大大得到提升。这就是要使用多线程技术、要学习多线程 的原因。这是多线程技术的优点,使用多线程也就是在使用异步。 多线程是异步的,所以千万不要把 Eclipse 里代码的顺序当成线程执行的顺序,线程 被调用的时机是随机的。 1.2 使用多线程 想学习一个技术就要“接近”它,所以在本节,首先用一个示例来接触一下线程。 注意 4   Java 多线程编程核心技术 一个进程正在运行时至少会有 1 个线程在运行,这种情况在 Java 中也是存在的。这些线 程在后台默默地执行,比如调用 public static void main() 方法的线程就是这样的,而且它是由 JVM 创建的。 创建示例项目 callMainMethodMainThread,创建 Test.java 类。代码如下: package test; public class Test { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } } 程序运行后的效果如图 1-5 所示。 图 1-5 主线程 main 在控制台中输出的 main 其实就是一个名称叫作 main 的线程在执行 main() 方法中的代 码。另外需要说明一下,在控制台输出的 main 和 main 方法没有任何的关系,仅仅是名字相 同而已。 1.2.1 继承 Thread 类 在 Java 的 JDK 开发包中,已经自带了对多线程技术的支持,可以很方便地进行多线程编 程。实现多线程编程的方式主要有两种,一种是继承 Thread 类,另一种是实现 Runnable 接口。 但在学习如何创建新的线程前,先来看看 Thread 类的结构,如下: public class Thread implements Runnable 从上面的源代码中可以发现,Thread 类实现了 Runnable 接口,它们之间具有多态关系。 其实,使用继承 Thread 类的方式创建新线程时,最大的局限就是不支持多继承,因为 Java 语言的特点就是单根继承,所以为了支持多继承,完全可以实现 Runnable 接口的方式, 一边实现一边继承。但用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。 本节来看一下第一种方法。创建名称为 t1 的 Java 项目,创建一个自定义的线程类 MyThread.java,此类继承自 Thread,并且重写 run 方法。在 run 方法中,写线程要执行的任 务的代码如下: package com.mythread.www; 第 1 章 Java 多线程技能   5 public class MyThread extends Thread { @Override public void run() { super.run(); System.out.println("MyThread"); } } 运行类代码如下: package test; import com.mythread.www.MyThread; public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); System.out.println(" 运行结束! "); } } 运行结果如图 1-6 所示。 图 1-6 运行结果 从图 1-6 中的运行结果来看,MyThread.java 类中的 run 方法执行的时间比较晚,这也说 明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。 线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法,所以就会出现先打印“运行结束!”后输出“MyThread”这样的结果了。 如果多次调用start() 方法,则会出现异常 Exception in thread "main" java.lang. IllegalThreadStateException。 上面介绍了线程的调用的随机性,下面将在名称为 randomThread 的 Java 项目中演示线 程的随机性。 创建自定义线程类 MyThread.java,代码如下: package mythread; public class MyThread extends Thread { @Override public void run() { 注意 6   Java 多线程编程核心技术 try { for (int i = 0; i < 10; i++) { int time = (int) (Math.random() * 1000); Thread.sleep(time); System.out.println("run=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 再创建运行类 Test.java,代码如下: package test; import mythread.MyThread; public class Test { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.setName("myThread"); thread.start(); for (int i = 0; i < 10; i++) { int time = (int) (Math.random() * 1000); Thread.sleep(time); System.out.println("main=" + Thread.currentThread().getName()); } } catch (InterruptedException e) { e.printStackTrace(); } } } 在代码中,为了展现出线程具有随机特性,所以使用随机数的形 式来使线程得到挂起的效果,从而表现出 CPU 执行哪个线程具有不确 定性。 Thread.java 类中的 start() 方法通知“线程规划器”此线程已经准备 就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排 一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,启动 线程,具有异步执行的效果。如果调用代码 thread.run() 就不是异步执行 了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理, 而是由 main 主线程来调用 run() 方法,也就是必须等 run() 方法中的代 码执行完后才可以执行后面的代码。 以异步的方式运行的效果如图 1-7 所示。 图 1-7  随机被执行 的线程 第 1 章 Java 多线程技能   7 另外还需要注意一下,执行 start() 方法的顺序不代表线程启动的顺序。创建测试用的项 目名称为 z,类 MyThread.java 代码如下: package extthread; public class MyThread extends Thread { private int i; public MyThread(int i) { super(); this.i = i; } @Override public void run() { System.out.println(i); } } 运行类 Test.java 代码如下: package test; import extthread.MyThread; public class Test { public static void main(String[] args) { MyThread t11 = new MyThread(1); MyThread t12 = new MyThread(2); MyThread t13 = new MyThread(3); MyThread t14 = new MyThread(4); MyThread t15 = new MyThread(5); MyThread t16 = new MyThread(6); MyThread t17 = new MyThread(7); MyThread t18 = new MyThread(8); MyThread t19 = new MyThread(9); MyThread t110 = new MyThread(10); MyThread t111 = new MyThread(11); MyThread t112 = new MyThread(12); MyThread t113 = new MyThread(13); t11.start(); t12.start(); t13.start(); t14.start(); t15.start(); t16.start(); t17.start(); t18.start(); t19.start(); t110.start(); t111.start(); t112.start(); t113.start(); } } 程序运行后的结果如图 1-8 所示。 图 1-8  线程启动顺序与 start() 执行顺序无关 8   Java 多线程编程核心技术 1.2.2 实现 Runnable 接口 如果欲创建的线程类已经有一个父类了,这时就不能再继承自 Thread 类了,因为 Java 不支持多继承,所以就需要实现 Runnable 接口来应对这样的情况。 创建项目 t2,继续创建一个实现 Runnable 接口的类 MyRunnable,代码如下: package myrunnable; public class MyRunnable implements Runnable { @Override public void run() { System.out.println(" 运行中 !"); } } 如何使用这个 MyRunnable.java 类呢?这就要看一下 Thread.java 的构造函数了,如 图 1-9 所示。 图 1-9 Thread 构造函数 在 Thread.java 类的8 个构造函数中,有两个构造函数Thread(Runnable target) 和 Thread(Runnable target,String name) 可以传递 Runnable 接口,说明构造函数支持传入一个 Runnable 接口的对象。运行类代码如下: public class Run { public static void main(String[] args) { Runnable runnable=new MyRunnable(); Thread thread=new Thread(runnable); thread.start(); System.out.println(" 运行结束 !"); } } 运行结果如图 1-10 所示。 图 1-10 所示的打印结果没有什么特殊之处。 第 1 章 Java 多线程技能   9 使用继承 Thread 类的方式来开发多线程应用程序在设计上是有局限性的,因为 Java 是 单根继承,不支持多继承,所以为了改变这种限制,可以使用实现 Runnable 接口的方式来实 现多线程技术。这也是上面的示例介绍的知识点。 另外需要说明的是,Thread.java 类也实现了 Runnable 接口,如图 1-11 所示。        图 1-10 运行结果 图 1-11 类 Thread 实现 Runnable 接口 那也就意味着构造函数 Thread(Runnable target) 不光可以传入 Runnable 接口的对象,还 可以传入一个 Thread 类的对象,这样做完全可以将一个 Thread 对象中的 run() 方法交由其他 的线程进行调用。 1.2.3 实例变量与线程安全 自定义线程类中的实例变量针对其他线程可以有共享与不共享之分,这在多个线程之间 进行交互时是很重要的一个技术点。 (1)不共享数据的情况 不共享数据的情况如图 1-12 所示。 图 1-12 不共享数据 下面通过一个示例来看下数据不共享情况。 创建实验用的 Java 项目,名称为 t3,MyThread.java 类代码如下: public class MyThread extends Thread { private int count = 5; public MyThread(String name) { super(); this.setName(name);// 设置线程名称 } @Override 10   Java 多线程编程核心技术 public void run() { super.run(); while (count > 0) { count--; System.out.println(" 由 " + this.currentThread().getName() + " 计算,count=" + count); } } } 运行类 Run.java 代码如下: public class Run { public static void main(String[] args) { MyThread a=new MyThread("A"); MyThread b=new MyThread("B"); MyThread c=new MyThread("C"); a.start(); b.start(); c.start(); } } 不共享数据运行结果如图 1-13 所示。 由图 1-13 可以看到,一共创建了 3 个线程,每个线程都有各自的 count 变量,自己减少 自己的 count 变量的值。这样的情况就是变量不共享,此示例并不存在多个线程访问同一个 实例变量的情况。 如果想实现 3 个线程共同对一个 count 变量进行减法操作的目的,该如何设计代码呢? (2)共享数据的情况 共享数据的情况如图 1-14 所示。 图 1-13 不共享数据的运行结果 图 1-14 共享数据 第 1 章 Java 多线程技能   11 共享数据的情况就是多个线程可以访问同一个变量,比如在实现投票功能的软件时,多 个线程可以同时处理同一个人的票数。 下面通过一个示例来看下数据共享情况。 创建 t4 测试项目,MyThread.java 类代码如下: public class MyThread extends Thread { private int count=5; @Override public void run() { super.run(); count--; // 此示例不要用 for 语句,因为使用同步后其他线程就得不到运行的机会了, // 一直由一个线程进行减法运算 System.out.println(" 由 "+this.currentThread().getName()+" 计 算, count="+count); } } 运行类 Run.java 代码如下: public class Run { public static void main(String[] args) { MyThread mythread=new MyThread(); Thread a=new Thread(mythread,"A"); Thread b=new Thread(mythread,"B"); Thread c=new Thread(mythread,"C"); Thread d=new Thread(mythread,"D"); Thread e=new Thread(mythread,"E"); a.start(); b.start(); c.start(); d.start(); e.start(); } } 运行结果如图 1-15 所示。 从图 1-15 中可以看到,线程 A 和 B 打印出的 count 值都是 3,说明 A 和 B 同时对 count 进行处理,产生了“非线程安全”问 题。而我们想要得到的打印结果却不是重复的,而是依次递减的。 在某些 JVM 中,i-- 的操作要分成如下 3 步: 1)取得原有 i 值。 2)计算 i-1。 3)对 i 进行赋值。 在这 3 个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。 图 1-15  共享数据运行 结果 12   Java 多线程编程核心技术 其实这个示例就是典型的销售场景:5 个销售员,每个销售员卖出一个货品后不可以得 出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品 数上继续减 1 操作。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行 减 1 操作。更改代码如下: public class MyThread extends Thread { private int count=5; @Override synchronized public void run() { super.run(); count--; System.out.println(" 由 "+this.currentThread().getName()+" 计 算, count="+count); } } 重新运行程序,就不会出现值一样的情况了,如图 1-16 所示。 通过在 run 方法前加入 synchronized 关键字,使多个线程在执 行 run 方法时,以排队的方式进行处理。当一个线程调用 run 前, 先判断 run 方法有没有被上锁,如果上锁,说明有其他线程正在 调用 run 方法,必须等其他线程对 run 方法调用结束后才可以执行 run 方法。这样也就实现了排队调用 run 方法的目的,也就达到了 按顺序对 count 变量减1的效果了。synchronized 可以在任意对象 及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。 当一个线程想要执行同步方法里面的代码时,线程首先尝试 去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行 synchronize 里面的代码。如 果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是 有多个线程同时去争抢这把锁。 本节中出现了一个术语“非线程安全”。非线程安全主要是指多个线程对同一个对象中 的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流 程。下面再用一个示例来学习一下如何解决“非线程安全”问题。 创建 t4_threadsafe 项目,来实现一下非线程安全的环境。LoginServlet.java 代码如下: package controller; // 本类模拟成一个 Servlet 组件 public class LoginServlet { private static String usernameRef; private static String passwordRef; public static void doPost(String username, String password) { try { usernameRef = username; 图 1-16  方法调用被 同步 第 1 章 Java 多线程技能   13 if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 线程 ALogin.java 代码如下: package extthread; import controller.LoginServlet; public class ALogin extends Thread { @Override public void run() { LoginServlet.doPost("a", "aa"); } } 线程 BLogin.java 代码如下: package extthread; import controller.LoginServlet; public class BLogin extends Thread { @Override public void run() { LoginServlet.doPost("b", "bb"); } } 运行类 Run.java 代码如下: public class Run { public static void main(String[] args) { ALogin a = new ALogin(); a.start(); BLogin b = new BLogin(); b.start(); } } 程序运行后的效果如图 1-17 所示。 解决这个“非线程安全”的方法也是使用 synchronized 关键字。更改代码如下: synchronized public static void doPost(String username, String password) { 14   Java 多线程编程核心技术 try { usernameRef = username; if (username.equals("a")) { Thread.sleep(5000); } passwordRef = password; System.out.println("username=" + usernameRef + " password=" + password); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } 程序运行后效果如图 1-18 所示。 图 1-17 非线程安全 图 1-18 排队进入方法 1.2.4 留意 i-- 与 System.out.println() 的异常 在前面章节中,解决非线程安全问题使用的是 synchronized 关键字,本节将通过程序案 例细化一下 println() 方法与 i++ 联合使用时“有可能”出现的另外一种异常情况,并说明其 中的原因。 创建名称为 sameNum 的项目,自定义线程 MyThread.java 代码如下: package extthread; public class MyThread extends Thread { private int i = 5; @Override public void run() { System.out.println("i=" + (i--) + " threadName=" + Thread.currentThread().getName()); // 注意:代码 i-- 由前面项目中单独一行运行改成在当前项目中在 println() 方法中直接进行打印 } } 运行类 Run.java 代码如下: package test; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread run = new MyThread(); Thread t1 = new Thread(run); 第 1 章 Java 多线程技能   15 Thread t2 = new Thread(run); Thread t3 = new Thread(run); Thread t4 = new Thread(run); Thread t5 = new Thread(run); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } } 程序运行后根据概率还是会出现非线程安全问题,如图 1-19 所示。 图 1-19 出现非线程安全问题 本实验的测试目的是:虽然 println() 方法在内部是同步的,但 i-- 的操作却是在进入 println() 之前发生的,所以有发生非线程安全问题的概率,如图 1-20 所示。 图 1-20 println 内部同步 所以,为了防止发生非线程安全问题,还是应继续使用同步方法。 16   Java 多线程编程核心技术 1.3 currentThread() 方法 currentThread() 方法可返回代码段正在被哪个线程调用的信息。下面通过一个示例进行说明。 创建 t6 项目,创建 Run1.java 类代码如下: public class Run1 { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } } 程序运行结果如图 1-21 所示。 图 1-21 运行结果 结果说明,main 方法被名为 main 的线程调用。 继续实验,创建 MyThread.java 类。代码如下: public class MyThread extends Thread { public MyThread() { System.out.println(" 构造方法的打印: " + Thread.currentThread().getName()); } @Override public void run() { System.out.println("run 方法的打印: " + Thread.currentThread().getName()); } } 运行类 Run2.java 代码如下: public class Run2 { public static void main(String[] args) { MyThread mythread = new MyThread(); mythread.start(); // mythread.run(); } } 第 1 章 Java 多线程技能   17 程序运行结果如图 1-22 所示。 从图 1-22 中的运行结果可以发现,MyThread.java 类的构造函数是被 main 线程调用的, 而 run 方法是被名称为 Thread-0 的线程调用的,run 方法是自动调用的方法。 文件 Run2.java 代码更改如下: public class Run2 { public static void main(String[] args) { MyThread mythread = new MyThread(); // mythread.start(); mythread.run(); } } 运行结果如图 1-23 所示。 图 1-22 运行结果 图 1-23 均被 main 主线程所调用 再来测试一个比较复杂的情况,创建测试用的项目 currentThreadExt,创建 Java 文件 CountOperate.java。代码如下: package mythread; public class CountOperate extends Thread { public CountOperate() { System.out.println("CountOperate---begin"); System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName()); System.out.println("this.getName()=" + this.getName()); System.out.println("CountOperate---end"); } @Override public void run() { System.out.println("run---begin"); System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName()); System.out.println("this.getName()=" + this.getName()); System.out.println("run---end"); 18   Java 多线程编程核心技术 } } 创建 Run.java 文件,代码如下: package test; import mythread.CountOperate; public class Run { public static void main(String[] args) { CountOperate c = new CountOperate(); Thread t1 = new Thread(c); t1.setName("A"); t1.start(); } } 程序运行结果如下: CountOperate---begin Thread.currentThread().getName()=main this.getName()=Thread-0 CountOperate---end run---begin Thread.currentThread().getName()=A this.getName()=Thread-0 run---end 1.4 isAlive() 方法 方法 isAlive() 的功能是判断当前的线程是否处于活动状态。 新建项目 t7,类文件 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { System.out.println("run=" + this.isAlive()); } } 运行 Run.java 代码如下: public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); System.out.println("begin ==" + mythread.isAlive()); mythread.start(); System.out.println("end ==" + mythread.isAlive()); } } 第 1 章 Java 多线程技能   19 程序运行结果如图 1-24 所示。 图 1-24 运行结果 方法 isAlive() 的作用是测试线程是否处于活动状态。什么是活动状态呢?活动状态 就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是 “存活”的。 需要说明一下,如以下代码: System.out.println("end ==" + mythread.isAlive()); 虽然在上面的示例中打印的值是 true,但此值是不确定的。打印 true 值是因为 mythread 线程还未执行完毕,所以输出 true。如果代码更改如下: public static void main(String[] args) throws InterruptedException { MyThread mythread = new MyThread(); System.out.println("begin ==" + mythread.isAlive()); mythread.start(); Thread.sleep(1000); System.out.println("end ==" + mythread.isAlive()); } 则上述代码运行的结果输出为 false,因为 mythread 对象已经在 1 秒之内执行完毕。 另外,在使用 isAlive() 方法时,如果将线程对象以构造参数的方式传递给 Thread 对象 进行 start() 启动时,运行的结果和前面示例是有差异的。造成这样的差异的原因还是来自于 Thread.currentThread() 和 this 的差异。下面测试一下这个实验。 创建测试用的 isaliveOtherTest 项目,创建 CountOperate.java 文件,代码如下: package mythread; public class CountOperate extends Thread { public CountOperate() { System.out.println("CountOperate---begin"); System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName()); System.out.println("Thread.currentThread().isAlive()=" + Thread.currentThread().isAlive()); System.out.println("this.getName()=" + this.getName()); System.out.println("this.isAlive()=" + this.isAlive()); System.out.println("CountOperate---end"); } @Override 20   Java 多线程编程核心技术 public void run() { System.out.println("run---begin"); System.out.println("Thread.currentThread().getName()=" + Thread.currentThread().getName()); System.out.println("Thread.currentThread().isAlive()=" + Thread.currentThread().isAlive()); System.out.println("this.getName()=" + this.getName()); System.out.println("this.isAlive()=" + this.isAlive()); System.out.println("run---end"); } } 创建 Run.java 文件,代码如下: package test; import mythread.CountOperate; public class Run { public static void main(String[] args) { CountOperate c = new CountOperate(); Thread t1 = new Thread(c); System.out.println("main begin t1 isAlive=" + t1.isAlive()); t1.setName("A"); t1.start(); System.out.println("main end t1 isAlive=" + t1.isAlive()); } } 程序运行结果如下: CountOperate---begin Thread.currentThread().getName()=main Thread.currentThread().isAlive()=true this.getName()=Thread-0 this.isAlive()=false CountOperate---end main begin t1 isAlive=false main end t1 isAlive=true run---begin Thread.currentThread().getName()=A Thread.currentThread().isAlive()=true this.getName()=Thread-0 this.isAlive()=false run---end 1.5 sleep() 方法 方法 sleep() 的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。 这个“正在执行的线程”是指 this.currentThread() 返回的线程。 第 1 章 Java 多线程技能   21 通过一个示例进行说明。创建项目 t8,类 MyThread1.java 代码如下: public class MyThread1 extends Thread { @Override public void run() { try { System.out.println("run threadName=" + this.currentThread().getName() + " begin"); Thread.sleep(2000); System.out.println("run threadName=" + this.currentThread().getName() + " end"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 运行类 Run1.java 代码如下: public class Run1 { public static void main(String[] args) { MyThread1 mythread = new MyThread1(); System.out.println("begin =" + System.currentTimeMillis()); mythread.run(); System.out.println("end =" + System.currentTimeMillis()); } } 直接调用 run() 方法,程序运行结果如图 1-25 所示。 继续实验,创建 MyThread2.java 代码如下: public class MyThread2 extends Thread { @Override public void run() { try { System.out.println("run threadName=" + this.currentThread().getName() + " begin =" + System.currentTimeMillis()); Thread.sleep(2000); System.out.println("run threadName=" + this.currentThread().getName() + " end =" + System.currentTimeMillis()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 22   Java 多线程编程核心技术 创建 Run2.java 代码如下: public class Run2 { public static void main(String[] args) { MyThread2 mythread = new MyThread2(); System.out.println("begin =" + System.currentTimeMillis()); mythread.start(); System.out.println("end =" + System.currentTimeMillis()); } } 使用 start() 方法启动线程,程序运行结果如图 1-26 所示。 图 1-25 将 main 线程暂停了 2 秒 图 1-26 运行结果 由于 main 线程与 MyThread2 线程是异步执行的,所以首先打印的信息为 begin 和 end。 而 MyThread2 线程是随后运行的,在最后两行打印 run begin 和 run end 相关的信息。 1.6 getId() 方法 getId() 方法的作用是取得线程的唯一标识。 创建测试用的项目 runThread,创建 Test.java 类,代码如下: package test; public class Test { public static void main(String[] args) { Thread runThread = Thread.currentThread(); System.out.println(runThread.getName() + " " + runThread.getId()); } } 程序运行后的效果如图 1-27 所示。 图 1-27 获取线程名称及 id 值 从打印的运行结果来看,当前执行代码的线程名称为 main,线程 id 值为 1。 第 1 章 Java 多线程技能   23 1.7 停止线程 停止线程是在多线程开发时很重要的技术点,掌握此技术可以对线程的停止进行有效的 处理。停止线程在 Java 语言中并不像 break 语句那样干脆,需要一些技巧性的处理。 使用 Java 内置支持多线程的类设计多线程应用是很常见的事情,然而,多线程给开发人 员带来了一些新的挑战,如果处理不好就会导致超出预期的行为并且难以定位错误。 本节将讨论如何更好地停止一个线程。停止一个线程意味着在线程处理完任务之前停掉 正在做的操作,也就是放弃当前的操作。虽然这看起来非常简单,但是必须做好防范措施, 以便达到预期的效果。停止一个线程可以使用 Thread.stop() 方法,但最好不用它。虽然它确 实可以停止一个正在运行的线程,但是这个方法是不安全的(unsafe),而且是已被弃用作废 的(deprecated),在将来的 Java 版本中,这个方法将不可用或不被支持。 大多数停止一个线程的操作使用 Thread.interrupt() 方法,尽管方法的名称是“停止,中 止”的意思,但这个方法不会终止一个正在运行的线程,还需要加入一个判断才可以完成线 程的停止。关于此知识点在后面有专门的章节进行介绍。 在 Java 中有以下 3 种方法可以终止正在运行的线程: 1)使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。 2)使用 stop 方法强行终止线程,但是不推荐使用这个方法,因为 stop 和 suspend 及 resume 一样,都是作废过期的方法,使用它们可能产生不可预料的结果。 3)使用 interrupt 方法中断线程。 这 3 种方法都会在后面的章节进行介绍。 1.7.1 停止不了的线程 本示例将调用interrupt() 方法来停止线程,但interrupt() 方法的使用效果并不像 for+break 语句那样,马上就停止循环。调用 interrupt() 方法仅仅是在当前线程中打了一个停 止的标记,并不是真的停止线程。 创建名称为 t11 的项目,文件 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { super.run(); for (int i = 0; i < 500000; i++) { System.out.println("i=" + (i + 1)); } } } 24   Java 多线程编程核心技术 运行类 Run.java 代码如下: public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } } } 程序运行结果如图 1-28 所示。 把 Eclipse 软件中的控制台的日志复制到 EditPlus 软件中,确认一下日志是否是 50 万行, 效果如图 1-29 所示。 图 1-28 在控制台输出 50 万行的日志 图 1-29 复制 50 万行日志 从运行的结果来看,调用 interrupt 方法并没有停止线程。如何停止线程呢? 1.7.2 判断线程是否是停止状态 在介绍如何停止线程的知识点前,先来看一下如何判断线程的状态是不是停止的。在 Java 的 SDK 中,Thread.java 类里提供了两种方法。 1)this.interrupted():测试当前线程是否已经中断。 2)this.isInterrupted():测试线程是否已经中断。 第 1 章 Java 多线程技能   25 interrupted() 方法的声明如图 1-30 所示。 isInterrupted () 方法的声明如图 1-31 所示。 图 1-30 interrupted 方法的声明 图 1-31 isInterrupted 方法的声明 那么这两个方法有什么区别呢?先来看看 this.interrupted() 方法的解释:测试当前线程 是否已经中断,当前线程是指运行 this.interrupted() 方法的线程。为了对此方法有更深入的了 解,创建项目,名称为 t12,类 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { super.run(); for (int i = 0; i < 500000; i++) { System.out.println("i=" + (i + 1)); } } } 类 Run.java 代码如下: public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.interrupt(); //Thread.currentThread().interrupt(); System.out.println(" 是否停止 1 ? ="+thread.interrupted()); System.out.println(" 是否停止 2 ? ="+thread.interrupted()); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } } 程序运行后的结果如图 1-32 所示。 类 Run.java 中虽然是在 thread 对象上调用以下代码: thread.interrupt(); 来停止 thread 对象所代表的线程,在后面又使用以下代码: 26   Java 多线程编程核心技术 System.out.println(" 是否停止 1 ? ="+thread.interrupted()); System.out.println(" 是否停止 2 ? ="+thread.interrupted()); 来判断 thread 对象所代表的线程是否停止,但从控制台打印的结果来看,线程并未停 止,这也就证明了 interrupted() 方法的解释:测试当前线程是否已经中断。这个“当前线程” 是 main,它从未中断过,所以打印的结果是两个 false。 如何使 main 线程产生中断效果呢?创建 Run2.java 代码如下: public class Run2 { public static void main(String[] args) { Thread.currentThread().interrupt(); System.out.println(" 是否停止 1 ? =" + Thread.interrupted()); System.out.println(" 是否停止 2 ? =" + Thread.interrupted()); System.out.println("end!"); } } 程序运行后的效果如图 1-33 所示。 图 1-32 运行结果 图 1-33 主线程 main 已是停止状态 从上述的结果来看,方法 interrupted() 的确判断出当前线程是否是停止状态。但为什么 第 2 个布尔值是 false 呢?查看一下官方帮助文档中对 interrupted 方法的解释: 测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次 调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次 调用检验完中断状态前,当前线程再次中断的情况除外)。 文档已经解释得很详细,interrupted() 方法具有清除状态的功能,所以第2 次调用 interrupted() 方法返回的值是 false。 介绍完 interrupted() 方法后再来看一下 isInterrupted() 方法,声明如下: public boolean isInterrupted() 从声明中可以看出 isInterrupted() 方法不是 static 的。 继续创建 Run3.java 类,代码如下: 第 1 章 Java 多线程技能   27 public class Run3 { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.interrupt(); System.out.println(" 是否停止 1 ? ="+thread.isInterrupted()); System.out.println(" 是否停止 2 ? ="+thread.isInterrupted()); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } } 程序运行结果如图 1-34 所示。 从结果中可以看到,方法 isInterrupted() 并未清除状态标 志,所以打印了两个 true。 最后,再来看一下这两个方法的解释。 1)this.interrupted() :测试当前线程是否已经是中断状态, 执行后具有将状态标志置清除为 false 的功能。 2)this.isInterrupted() :测试线程 Thread 对象是否已经 是中断状态,但不清除状态标志。 1.7.3 能停止的线程——异常法 有了前面学习过的知识点,就可在线程中用 for 语句来判断一下线程是否是停止状态, 如果是停止状态,则后面的代码不再运行即可。 创建实验用的项目 t13,类 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { super.run(); for (int i = 0; i < 500000; i++) { if (this.interrupted()) { System.out.println(" 已经是停止状态了 ! 我要退出了 !"); break; } System.out.println("i=" + (i + 1)); } } } 图 1-34 已经是停止状态 28   Java 多线程编程核心技术 类 Run.java 代码如下: public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } } 程序运行结果如图 1-35 所示。 上面的示例虽然停止了线程,但如果 for 语句下面还有语句,还是会继续运行的。创建 测试项目 t13forprint,类 MyThread.java 代码如下: package exthread; public class MyThread extends Thread { @Override public void run() { super.run(); for (int i = 0; i < 500000; i++) { if (this.interrupted()) { System.out.println(" 已经是停止状态了 ! 我要退出了 !"); break; } System.out.println("i=" + (i + 1)); } System.out.println(" 我被输出,如果此代码是 for 又继续运行,线程并未停止! "); } } 文件 Run.java 代码如下: package test; import exthread.MyThread; import exthread.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); 图 1-35 线程已经是停止状态 第 1 章 Java 多线程技能   29 } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } } 程序运行后输出结果如图 1-36 所示。 该如何解决语句继续运行的问题呢?看 一下更新后的代码。 创建 t13_1 项目,类 MyThread.java 代码 如下: package exthread; public class MyThread extends Thread { @Override public void run() { super.run(); try { for (int i = 0; i < 500000; i++) { if (this.interrupted()) { System.out.println(" 已经是停止状态了 ! 我要退出了 !"); throw new InterruptedException(); } System.out.println("i=" + (i + 1)); } System.out.println(" 我在 for 下面 "); } catch (InterruptedException e) { System.out.println(" 进 MyThread.java 类 run 方法中的 catch 了! "); e.printStackTrace(); } } } 类 Run.java 代码如下: package test; import exthread.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(2000); thread.interrupt(); } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); 图 1-36 for 后面的语句继续运行 30   Java 多线程编程核心技术 } System.out.println("end!"); } } 运行结果如图 1-37 所示。 图 1-37 运行结果 1.7.4 在沉睡中停止 如果线程在 sleep() 状态下停止线程,会是什么效果呢? 新建项目 t14,类 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { super.run(); try { System.out.println("run begin"); Thread.sleep(200000); System.out.println("run end"); } catch (InterruptedException e) { System.out.println(" 在沉睡中被停止 ! 进入 catch!"+this.isInterrupted()); e.printStackTrace(); } } } 文件 Run.java 代码如下: public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(200); thread.interrupt(); 第 1 章 Java 多线程技能   31 } catch (InterruptedException e) { System.out.println("main catch"); e.printStackTrace(); } System.out.println("end!"); } } 程序运行后的效果如图 1-38 所示。 从打印的结果来看,如果在 sleep 状态下 停止某一线程,会进入 catch 语句,并且清除 停止状态值,使之变成 false。 前一个实验是先 sleep 然后再用 interrupt() 停止,与之相反的操作在学习线程时也要注意。 新建项目 t15,类 MyThread.java 代码如下: public class MyThread extends Thread { @Override public void run() { super.run(); try { for(int i=0;i<100000;i++){ System.out.println("i="+(i+1)); } System.out.println("run begin"); Thread.sleep(200000); System.out.println("run end"); } catch (InterruptedException e) { System.out.println(" 先停止,再遇到了 sleep! 进入 catch!"); e.printStackTrace(); } } } 类 Run.java 代码如下: public class Run { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); thread.interrupt(); System.out.println("end!"); } } 运行结果如图 1-39 所示。 控制台最下面的输出如图 1-40 所示。 图 1-38 运行结果 图 1-39 先执行 interrupt 停止线程 32   Java 多线程编程核心技术 图 1-40 遇到 sleep 提示异常 1.7.5 能停止的线程——暴力停止 使用 stop() 方法停止线程则是非常暴力的。 示例项目名称 useStopMethodThreadTest,文件 MyThread.java 代码如下: package testpackage; public class MyThread extends Thread { private int i = 0; @Override public void run() { try { while (true) { i++; System.out.println("i=" + i); Thread.sleep(1000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 文件 Run.java 代码如下: package test.run; import testpackage.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(8000); thread.stop(); } catch (InterruptedException e) { 第 1 章 Java 多线程技能   33 // TODO Auto-generated catch block e.printStackTrace(); } } } 程序运行后的效果如图 1-41 所示。 图 1-41 线程被暴力停止(stop)运行后图标呈灰色 1.7.6 方法 stop() 与 java.lang.ThreadDeath 异常 调用 stop() 方法时会抛出 java.lang.ThreadDeath 异常,但在通常的情况下,此异常不需 要显式地捕捉。 创建测试用的项目 runMethodUseStopMethod,文件 MyThread.java 代码如下: package testpackage; public class MyThread extends Thread { @Override public void run() { try { this.stop(); } catch (ThreadDeath e) { System.out.println(" 进入了 catch() 方法! "); e.printStackTrace(); } } } 文件 Run.java 代码如下: package test.run; import testpackage.MyThread; public class Run { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } 34   Java 多线程编程核心技术 程序运行后的效果如图 1-42 所示。 方法 stop() 已经被作废,因为如果强 制让线程停止则有可能使一些清理性的工 作得不到完成。另外一个情况就是对锁定 的对象进行了“解锁”,导致数据得不到 同步的处理,出现数据不一致的问题。 1.7.7 释放锁的不良后果 使用 stop() 释放锁将会给数据造成不一致性的结果。如果出现这样的情况,程序处理的 数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意。 来看一个示例。 创建项目 stopThrowLock,文件 SynchronizedObject.java 代码如下: package testpackage; public class SynchronizedObject { private String username = "a"; private String password = "aa"; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } synchronized public void printString(String username, String password) { try { this.username = username; Thread.sleep(100000); this.password = password; } catch (InterruptedException e) { e.printStackTrace(); } } } 文件 MyThread.java 代码如下: package testpackage; public class MyThread extends Thread { private SynchronizedObject object; 图 1-42 进入 catch 异常 第 1 章 Java 多线程技能   35 public MyThread(SynchronizedObject object) { super(); this.object = object; } @Override public void run() { object.printString("b", "bb"); } } 文件 Run.java 代码如下: package test.run; import testpackage.MyThread; import testpackage.SynchronizedObject; public class Run { public static void main(String[] args) { try { SynchronizedObject object = new SynchronizedObject(); MyThread thread = new MyThread(object); thread.start(); Thread.sleep(500); thread.stop(); System.out.println(object.getUsername() + " " + object.getPassword()); } catch (InterruptedException e) { e.printStackTrace(); } } } 程序运行结果如图 1-43 所示。 图 1-43 强制 stop 造成数据不一致 由于 stop() 方法已经在 JDK 中被标明是“作废 / 过期”的方法,显然它在功能上具有缺 陷,所以不建议在程序中使用 stop() 方法。 1.7.8 使用 return 停止线程 将方法 interrupt() 与 return 结合使用也能实现停止线程的效果。 创建测试用的项目 useReturnInterrupt,线程类 MyThread.java 代码如下: package extthread; public class MyThread extends Thread { 36   Java 多线程编程核心技术 @Override public void run() { while (true) { if (this.isInterrupted()) { System.out.println(" 停止了 !"); return; } System.out.println("timer=" + System.currentTimeMillis()); } } } 运行类 Run.java 代码如下: package test.run; import extthread.MyThread; public class Run { public static void main(String[] args) throws InterruptedException { MyThread t=new MyThread(); t.start(); Thread.sleep(2000); t.interrupt(); } } 程序运行结果如图 1-44 所示。 不过还是建议使用“抛异常”的方法来实现线程 的停止,因为在 catch 块中还可以将异常向上抛,使 线程停止的事件得以传播。 1.8 暂停线程 暂停线程意味着此线程还可以恢复运行。在 Java 多线程中,可以使用 suspend() 方法暂 停线程,使用 resume() 方法恢复线程的执行。 1.8.1 suspend 与 resume 方法的使用 本节将讲述如何使用 suspend 与 resume 方法。 创建测试用的项目 suspend_resume_test,文件 MyThread.java 代码如下: package mythread; public class MyThread extends Thread { private long i = 0; public long getI() { 图 1-44 成功停止运行 第 1 章 Java 多线程技能   37 return i; } public void setI(long i) { this.i = i; } @Override public void run() { while (true) { i++; } } } 文件 Run.java 代码如下: package test.run; import mythread.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(5000); // A 段 thread.suspend(); System.out.println("A= " + System.currentTimeMillis() + " i=" + thread.getI()); Thread.sleep(5000); System.out.println("A= " + System.currentTimeMillis() + " i=" + thread.getI()); // B 段 thread.resume(); Thread.sleep(5000); // C 段 thread.suspend(); System.out.println("B= " + System.currentTimeMillis() + " i=" + thread.getI()); Thread.sleep(5000); System.out.println("B= " + System.currentTimeMillis() + " i=" + thread.getI()); } catch (InterruptedException e) { e.printStackTrace(); } } } 程序运行后的结果如图 1-45 所示。 从控制台打印的时间上来看,线程的确被暂停了, 而且还可以恢复成运行的状态。 图 1-45 暂停与恢复线程 38   Java 多线程编程核心技术 1.8.2 suspend 与 resume 方法的缺点——独占 在使用 suspend 与 resume 方法时,如果使用不当,极易造成公共的同步对象的独占,使 得其他线程无法访问公共同步对象。 创建 suspend_resume_deal_lock 项目,文件 SynchronizedObject.java 代码如下: package testpackage; public class SynchronizedObject { synchronized public void printString() { System.out.println("begin"); if (Thread.currentThread().getName().equals("a")) { System.out.println("a 线程永远 suspend 了! "); Thread.currentThread().suspend(); } System.out.println("end"); } } 文件 Run.java 代码如下: package test.run; import testpackage.SynchronizedObject; public class Run { public static void main(String[] args) { try { final SynchronizedObject object = new SynchronizedObject(); Thread thread1 = new Thread() { @Override public void run() { object.printString(); } }; thread1.setName("a"); thread1.start(); Thread.sleep(1000); Thread thread2 = new Thread() { @Override public void run() { System.out .println("thread2 启动了,但进入不了 printString() 方法! 只打印 1 个 begin"); System.out .println(" 因为 printString() 方法被 a 线程锁定并且永远 suspend 暂停了! "); object.printString(); } }; thread2.start(); } catch (InterruptedException e) { 第 1 章 Java 多线程技能   39 // TODO Auto-generated catch block e.printStackTrace(); } } } 程序运行后的结果如图 1-46 所示。 还有另外一种独占锁的情况也要格外注意,稍有不慎,就会掉进“坑”里。创建测试用 的项目 suspend_resume_LockStop,类 MyThread.java 代码如下: package mythread; public class MyThread extends Thread { private long i = 0; @Override public void run() { while (true) { i++; } } } 类 Run.java 代码如下: package test.run; import mythread.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.start(); Thread.sleep(1000); thread.suspend(); System.out.println("main end!"); } catch (InterruptedException e) { e.printStackTrace(); } } } 程序运行后顺序打印的信息如图 1-47 所示。 图 1-46 独占并锁死了 printString 方法        图 1-47 控制台打印 main end 信息 但如果将线程类 MyThread.java 更改如下: 40   Java 多线程编程核心技术 package mythread; public class MyThread extends Thread { private long i = 0; @Override public void run() { while (true) { i++; System.out.println(i); } } } 再次运行程序,控制台将不打印 main end,运行结果如图 1-48 所示。 图 1-48 不打印 main end 信息 出现这样情况的原因是,当程序运行到 println() 方法内部停止时,同步锁未被释放。方 法 println() 源代码如图 1-49 所示。 图 1-49 锁不被释放 这导致当前 PrintStream 对象的 println() 方法一直呈“暂停”状态,并且“锁未释放”, 而 main() 方法中的代码 System.out.println("main end!"); 迟迟不能执行打印。 虽然 suspend() 方法是过期作废的方法,但还是有必要研究它过期作废的原因,这是很 有意义的。 1.8.3 suspend 与 resume 方法的缺点——不同步 在使用 suspend 与 resume 方法时也容易出现因为线程的暂停而导致数据不同步的情况。 创建项目 suspend_resume_nosameValue,文件 MyObject.java 代码如下: package myobject; 第 1 章 Java 多线程技能   41 public class MyObject { private String username = "1"; private String password = "11"; public void setValue(String u, String p) { this.username = u; if (Thread.currentThread().getName().equals("a")) { System.out.println(" 停止 a 线程! "); Thread.currentThread().suspend(); } this.password = p; } public void printUsernamePassword() { System.out.println(username + " " + password); } } 文件 Run.java 代码如下: package test; import myobject.MyObject; public class Run { public static void main(String[] args) throws InterruptedException { final MyObject myobject = new MyObject(); Thread thread1 = new Thread() { public void run() { myobject.setValue("a", "aa"); }; }; thread1.setName("a"); thread1.start(); Thread.sleep(500); Thread thread2 = new Thread() { public void run() { myobject.printUsernamePassword(); }; }; thread2.start(); } } 程序运行结果如图 1-50 所示。 图 1-50 运行结果 程序运行的结果出现值不同步的情况,所以在程序中使用 suspend() 方法要格外注意。 关于如何解决这些问题,请看后面的章节。 42   Java 多线程编程核心技术 1.9 yield 方法 yield() 方法的作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行时 间。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。 在本示例中,可以取得运行的时间,作为比较结果,测试 yield 方法的使用效果。 创建 t17 项目,MyThread.java 文件代码如下: package extthread; public class MyThread extends Thread { @Override public void run() { long beginTime = System.currentTimeMillis(); int count = 0; for (int i = 0; i < 50000000; i++) { // Thread.yield(); count = count + (i + 1); } long endTime = System.currentTimeMillis(); System.out.println(" 用时:" + (endTime - beginTime) + " 毫秒! "); } } 文件 Run.java 代码如下: package test; import extthread.MyThread; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } 程序运行后的结果如图 1-51 所示。 将代码: // Thread.yield(); 去掉注释符号,再次运行,运行时间如图 1-52 所示。 图 1-51 CPU 独占时间片 图 1-52 将 CPU 让给其他资源导致速度变慢 第 1 章 Java 多线程技能   43 1.10 线程的优先级 在操作系统中,线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就 是 CPU 优先执行优先级较高的线程对象中的任务。 设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。 设置线程的优先级使用 setPriority() 方法,此方法在 JDK 的源代码如下: public final void setPriority(int newPriority) { ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException(); } if ((g = getThreadGroup()) != null) { if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority(); } setPriority0(priority = newPriority); } } 在 Java 中,线程的优先级分为 1 ~ 10 这 10 个等级,如果小于 1 或大于 10,则 JDK 抛 出异常 throw new IllegalArgumentException()。 JDK 中使用 3 个常量来预置定义优先级的值,代码如下: public final static int MIN_PRIORITY = 1; public final static int NORM_PRIORITY = 5; public final static int MAX_PRIORITY = 10; 1.10.1 线程优先级的继承特性 在 Java 中,线程的优先级具有继承性,比如 A 线程启动 B 线程,则 B 线程的优先级与 A 是一样的。 创建 t18 项目,创建 MyThread1.java 文件,代码如下: package extthread; public class MyThread1 extends Thread { @Override public void run() { System.out.println("MyThread1 run priority=" + this.getPriority()); MyThread2 thread2 = new MyThread2(); thread2.start(); } } 创建 MyThread2.java 文件,代码如下: 44   Java 多线程编程核心技术 package extthread; public class MyThread2 extends Thread { @Override public void run() { System.out.println("MyThread2 run priority=" + this.getPriority()); } } 文件 Run.java 代码如下: package test; import extthread.MyThread1; public class Run { public static void main(String[] args) { System.out.println("main thread begin priority=" + Thread.currentThread().getPriority()); // Thread.currentThread().setPriority(6); System.out.println("main thread end priority=" + Thread.currentThread().getPriority()); MyThread1 thread1 = new MyThread1(); thread1.start(); } } 程序运行后的效果如图 1-53 所示。 将代码: // Thread.currentThread().setPriority(6); 前的注释符号去掉,再次运行 Run.java 文件,显示结果如图 1-54 所示。 图 1-53 优先级被继承 图 1-54 优先级被更改再继续继承 1.10.2 优先级具有规则性 虽然使用 setPriority() 方法可以设置线程的优先级,但还没有看到设置优先级所带来的效果。 创建名称为 t19 的项目,文件 MyThread1.java 代码如下: package extthread; import java.util.Random; public class MyThread1 extends Thread { @Override public void run() { long beginTime = System.currentTimeMillis(); 第 1 章 Java 多线程技能   45 long addResult = 0; for (int j = 0; j < 10; j++) { for (int i = 0; i < 50000; i++) { Random random = new Random(); random.nextInt(); addResult = addResult + i; } } long endTime = System.currentTimeMillis(); System.out.println(" ★★★★★thread 1 use time=" + (endTime - beginTime)); } } 文件 MyThread2.java 代码如下: package extthread; import java.util.Random; public class MyThread2 extends Thread { @Override public void run() { long beginTime = System.currentTimeMillis(); long addResult = 0; for (int j = 0; j < 10; j++) { for (int i = 0; i < 50000; i++) { Random random = new Random(); random.nextInt(); addResult = addResult + i; } } long endTime = System.currentTimeMillis(); System.out.println(" ☆☆☆☆☆thread 2 use time=" + (endTime - beginTime)); } } 文件 Run.java 代码如下。 package test; import extthread.MyThread1; import extthread.MyThread2; public class Run { public static void main(String[] args) { for (int i = 0; i < 5; i++) { MyThread1 thread1 = new MyThread1(); thread1.setPriority(10); thread1.start(); MyThread2 thread2 = new MyThread2(); thread2.setPriority(1); thread2.start(); } } } 46   Java 多线程编程核心技术 文件 Run.java 在运行 3 次后的打印结果如图 1-55 所示。 图 1-55 高优先级的线程总是先执行完 从图 1-55 中可以发现,高优先级的线程总是大部分先执行完,但不代表高优先级的线 程全部先执行完。另外,不要以为 MyThread1 线程先被 main 线程所调用就会先执行完,出 现这样的结果全是因为 MyThread1 线程的优先级是最高值为 10 造成的。当线程优先级的等 级差距很大时,谁先执行完和代码的调用顺序无关。为了验证这个结论,继续实验,改变 Run.java 代码如下: public class Run { public static void main(String[] args) { for (int i = 0; i < 5; i++) { MyThread1 thread1 = new MyThread1(); thread1.setPriority(1); thread1.start(); MyThread2 thread2 = new MyThread2(); thread2.setPriority(10); thread2.start(); } } } 文件 Run.java 在运行 3 次后的打印结果如图 1-56 所示。 图 1-56 大部分 thread2 先执行完 第 1 章 Java 多线程技能   47 从图 1-56 中可以发现,大部分的 thread2 先执行完,也就验证了线程的优先级与代码执 行顺序无关,出现这样的结果是因为 MyThread2 的优先级是最高的,说明线程的优先级具有 一定的规则性,也就是 CPU 尽量将执行资源让给优先级比较高的线程。 1.10.3 优先级具有随机性 前面案例介绍了线程的优先级较高则优先执行完 run() 方法中的任务,但这个结果不能 说的太肯定,因为线程的优先级还具有“随机性”,也就是优先级较高的线程不一定每一次 都先执行完。 创建名称为 t20 的项目,文件 MyThread1.java 代码如下: package extthread; import java.util.Random; public class MyThread1 extends Thread { @Override public void run() { long beginTime = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { Random random = new Random(); random.nextInt(); } long endTime = System.currentTimeMillis(); System.out.println(" ★★★★★thread 1 use time=" + (endTime - beginTime)); } } 文件 MyThread2.java 代码如下: package extthread; import java.util.Random; public class MyThread2 extends Thread { @Override public void run() { long beginTime = System.currentTimeMillis(); for (int i = 0; i < 1000; i++) { Random random = new Random(); random.nextInt(); } long endTime = System.currentTimeMillis(); System.out.println(" ☆☆☆☆☆thread 2 use time=" + (endTime - beginTime)); } } 文件 Run.java 代码如下: 48   Java 多线程编程核心技术 package test; import extthread.MyThread1; import extthread.MyThread2; public class Run { public static void main(String[] args) { for (int i = 0; i < 5; i++) { MyThread1 thread1 = new MyThread1(); thread1.setPriority(5); thread1.start(); MyThread2 thread2 = new MyThread2(); thread2.setPriority(6); thread2.start(); } } } 为了让结果体现“随机性”,所以两个线程的优先级一个设置为 5,另一个设置为 6,让 优先级接近一些。 文件 Run.java 在运行 6 次后的打印结果如图 1-57 所示。 图 1-57 运行 6 次后的结果 那么,根据此实验可以得出一个结论,不要把线程的优先级与运行结果的顺序作为衡 量的标准,优先级较高的线程并不一定每一次都先执行完 run() 方法中的任务,也就是说, 线程优先级与打印顺序无关,不要将这两者的关系相关联,它们的关系具有不确定性和随 机性。 第 1 章 Java 多线程技能   49 1.10.4 看谁运行得快 创建实验用的项目 countPriority,创建两个线程类,代码如图 1-58 所示。 图 1-58 两个线程类代码 创建类 Run.java,代码如下: package test; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { try { ThreadA a = new ThreadA(); a.setPriority(Thread.NORM_PRIORITY - 3); a.start(); ThreadB b = new ThreadB(); b.setPriority(Thread.NORM_PRIORITY + 3); b.start(); Thread.sleep(20000); a.stop(); b.stop(); System.out.println("a=" + a.getCount()); System.out.println("b=" + b.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } } } 程序运行结果如图 1-59 所示。 图 1-59 优先级高的运行得快 50   Java 多线程编程核心技术 1.11 守护线程 在 Java 线程中有两种线程,一种是用户线程,另一种是守护线程。 守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线 程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程 了,则垃圾回收线程也就没有存在的必要了,自动销毁。用个比较通俗的比喻来解释一下 “守护线程”:任何一个守护线程都是整个 JVM 中所有非守护线程的“保姆”,只要当前 JVM 实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程 结束时,守护线程才随着 JVM 一同结束工作。Daemon 的作用是为其他线程的运行提供便利 服务,守护线程最典型的应用就是 GC(垃圾回收器),它就是一个很称职的守护者。 创建项目 daemonThread,文件 MyThread.java 代码如下: package testpackage; public class MyThread extends Thread { private int i = 0; @Override public void run() { try { while (true) { i++; System.out.println("i=" + (i)); Thread.sleep(1000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 文件 Run.java 代码如下: package test.run; import testpackage.MyThread; public class Run { public static void main(String[] args) { try { MyThread thread = new MyThread(); thread.setDaemon(true); thread.start(); Thread.sleep(5000); System.out.println(" 我离开 thread 对象也不再打印了,也就是停止了! "); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); 第 1 章 Java 多线程技能   51 } } } 程序运行后的效果如图 1-60 所示。 图 1-60 守护线程也退出了 1.12 本章小结 本章介绍了 Thread 类的 API,在使用这些 API 的过程中,会出现一些意想不到的情况, 其实这也是多线程具有不可预知性的一个体现。学习和掌握这些常用情况,也就掌握了多线 程开发的命脉与习性,是学习多线程更深层知识的基础。 第 2 章 对象及变量的并发访问 本章主要介绍 Java 多线程中的同步,也就是如何在 Java 语言中写出线程安全的程序, 如何在 Java 语言中解决非线程安全的相关问题。多线程中的同步问题是学习多线程的重中之 重,这个技术在其他的编程语言中也涉及,如 C++ 或 C#。 本章应该着重掌握如下技术点: ‰‰ synchronized 对象监视器为 Object 时的使用。 ‰‰ synchronized 对象监视器为 Class 时的使用。 ‰‰ 非线程安全是如何出现的。 ‰‰ 关键字 volatile 的主要作用。 ‰‰ 关键字 volatile 与 synchronized 的区别及使用情况。 2.1 synchronized 同步方法 在第 1 章中已经接触“线程安全”与“非线程安全”相关的技术点,它们是学习多线 程技术时一定会遇到的经典问题。“非线程安全”其实会在多个线程对同一个对象中的实例 变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。 而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。此知 识点在第 1 章也介绍,但本章将细化线程并发访问的内容,在细节上更多接触在并发时变量 值的处理方法。 Chapter 2 第 2 章 对象及变量的并发访问   53 2.1.1 方法内的变量为线程安全 “非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非 线程安全”问题,所得结果也就是“线程安全”的了。 下面的示例项目就是实现方法内部声明一个变量时,是不存在“非线程安全”问题的。 创建 t1 项目,HasSelfPrivateNum.java 文件代码如下: package service; public class HasSelfPrivateNum { public void addI(String username) { try { int num = 0; if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 文件 ThreadA.java 代码如下: package extthread; import service.HasSelfPrivateNum; public class ThreadA extends Thread { private HasSelfPrivateNum numRef; public ThreadA(HasSelfPrivateNum numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("a"); } } 文件 ThreadB.java 代码如下: 54   Java 多线程编程核心技术 package extthread; import service.HasSelfPrivateNum; public class ThreadB extends Thread { private HasSelfPrivateNum numRef; public ThreadB(HasSelfPrivateNum numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("b"); } } 文件 Run.java 代码如下: package test; import service.HasSelfPrivateNum; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); ThreadA athread = new ThreadA(numRef); athread.start(); ThreadB bthread = new ThreadB(numRef); bthread.start(); } } 程序运行后的效果如图 2-1 所示。 图 2-1 方法中的变量呈线程安全状态 可见,方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变 量是私有的特性造成的。 2.1.2 实例变量非线程安全 如果多个线程共同访问 1 个对象中的实例变量,则有可能出现“非线程安全”问题。 用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。此情 况在第 1 章中非线程安全的案例演示过。 第 2 章 对象及变量的并发访问   55 如果对象仅有 1 个实例变量,则有可能出现覆盖的情况。 创建 t2 项目,HasSelfPrivateNum.java 文件代码如下: package service; public class HasSelfPrivateNum { private int num = 0; public void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 文件 ThreadA.java 代码如下: package extthread; import service.HasSelfPrivateNum; public class ThreadA extends Thread { private HasSelfPrivateNum numRef; public ThreadA(HasSelfPrivateNum numRef) { super(); this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("a"); } } 文件 ThreadB.java 代码如下: package extthread; import service.HasSelfPrivateNum; public class ThreadB extends Thread { private HasSelfPrivateNum numRef; public ThreadB(HasSelfPrivateNum numRef) { super(); 56   Java 多线程编程核心技术 this.numRef = numRef; } @Override public void run() { super.run(); numRef.addI("b"); } } 文件 Run.java 代码如下: package test; import service.HasSelfPrivateNum; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { HasSelfPrivateNum numRef = new HasSelfPrivateNum(); ThreadA athread = new ThreadA(numRef); athread.start(); ThreadB bthread = new ThreadB(numRef); bthread.start(); } } 程序运行后的结果如图 2-2 所示。 图 2-2 单例模式中的实例变量呈非线程安全状态 本实验是两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的 实例变量,则有可能会出现“非线程安全”问题。此示例的知识点在前面已经介绍过,只需 要在 public void addI(String username) 方法前加关键字 synchronized 即可。更改后的代码如下: package service; public class HasSelfPrivateNum { private int num = 0; synchronized public void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { 第 2 章 对象及变量的并发访问   57 num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 程序再次运行结果如图 2-3 所示。 图 2-3 同步后线程安全了 实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。本实验由于 是同步访问,所以先打印出 a,然后打印出 b。 2.1.3 多个对象多个锁 再来看一个实验,创建项目名称为twoObjectTwoLock,创建HasSelfPrivateNum.java 类,代码如下: package service; public class HasSelfPrivateNum { private int num = 0; synchronized public void addI(String username) { try { if (username.equals("a")) { num = 100; System.out.println("a set over!"); Thread.sleep(2000); } else { num = 200; System.out.println("b set over!"); } System.out.println(username + " num=" + num); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 58   Java 多线程编程核心技术 上面的代码中有同步方法 addI,说明此方法应该被顺序调用。 创建线程 ThreadA.java 和 ThreadB.java 代码,如图 2-4 所示。 图 2-4 两个线程类代码 类 Run.java 代码如下: package test; import service.HasSelfPrivateNum; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { HasSelfPrivateNum numRef1 = new HasSelfPrivateNum(); HasSelfPrivateNum numRef2 = new HasSelfPrivateNum(); ThreadA athread = new ThreadA(numRef1); athread.start(); ThreadB bthread = new ThreadB(numRef2); bthread.start(); } } 创建了 2 个 HasSelfPrivateNum.java 类的对象,程序运行的结 果如图 2-5 所示。 上面示例是两个线程分别访问同一个类的两个不同实例的相 同名称的同步方法,效果却是以异步的方式运行的。本示例由于 创建了 2 个业务对象,在系统中产生出 2 个锁,所以运行结果是 图 2-5 无同步时各自有锁 第 2 章 对象及变量的并发访问   59 异步的,打印的效果就是先打印 b,然后打印 a。 从上面程序运行结果来看,虽然在 HasSelfPrivateNum.java 中使用了 synchronized 关键 字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢? 关键字 synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁, 所以在上面的示例中,哪个线程先执行带 synchronized 关键字的方法,哪个线程就持有该方 法所属对象的锁 Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个 对象。 但如果多个线程访问多个对象,则 JVM 会创建多个锁。上面的示例就是创建了 2 个 HasSelfPrivateNum.java 类的对象,所以就会产生出 2 个锁。 同步的单词为 synchronized,异步的单词为 asynchronized。 2.1.4 synchronized 方法与锁对象 为了证明前面讲述线程锁的是对象,创建实验用的项目 synchronizedMethodLockObject, 类 MyObject.java 文件代码如下: package extobject; public class MyObject { public void methodA() { try { System.out.println("begin methodA threadName=" + Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("end"); } catch (InterruptedException e) { e.printStackTrace(); } } } 自定义线程类 ThreadA.java 代码如下: package extthread; import extobject.MyObject; public class ThreadA extends Thread { private MyObject object; public ThreadA(MyObject object) { super(); this.object = object; } @Override public void run() { super.run(); 60   Java 多线程编程核心技术 object.methodA(); } } 自定义线程类 ThreadB.java 代码如下: package extthread; import extobject.MyObject; public class ThreadB extends Thread { private MyObject object; public ThreadB(MyObject object) { super(); this.object = object; } @Override public void run() { super.run(); object.methodA(); } } 运行类 Run.java 代码如下: package test.run; import extobject.MyObject; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { MyObject object = new MyObject(); ThreadA a = new ThreadA(object); a.setName("A"); ThreadB b = new ThreadB(object); b.setName("B"); a.start(); b.start(); } } 程序运行后的效果如图 2-6 所示。 更改 MyObject.java 代码如下: package extobject; public class MyObject { synchronized public void methodA() { try { System.out.println("begin methodA threadName=" + Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("end"); } catch (InterruptedException e) { e.printStackTrace(); 第 2 章 对象及变量的并发访问   61 } } } 如上面代码所示,在 methodA 方法前加入了关键字 synchronized 进行同步处理。程序再 次运行效果如图 2-7 所示。 图 2-6 两个线程可一同进入 methodA 方法 图 2-7 排队进入方法 通过上面的实验得到结论,调用用关键字 synchronized 声明的方法一定是排队运行的。 另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共 享资源,那么根本就没有同步的必要。 那其他的方法在被调用时会是什么效果呢?如何查看到 Lock 锁对象的效果呢?继续新 建实验用的项目 synchronizedMethodLockObject2,类文件 MyObject.java 代码如下: package extobject; public class MyObject { synchronized public void methodA() { try { System.out.println("begin methodA threadName=" + Thread.currentThread().getName()); Thread.sleep(5000); System.out.println("end endTime=" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } public void methodB() { try { System.out.println("begin methodB threadName=" + Thread.currentThread().getName() + " begin time=" + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("end"); } catch (InterruptedException e) { e.printStackTrace(); } } } 两个自定义线程类分别调用不同的方法,代码如图 2-8 所示。 62   Java 多线程编程核心技术 图 2-8 调用不同方法的线程类 文件 Run.java 代码如下: package test.run; import extobject.MyObject; import extthread.ThreadA; import extthread.ThreadB; public class Run { public static void main(String[] args) { MyObject object = new MyObject(); ThreadA a = new ThreadA(object); a.setName("A"); ThreadB b = new ThreadB(object); b.setName("B"); a.start(); b.start(); } } 程序运行结果如图 2-9 所示。 通过上面的实验可以得知,虽然线程 A 先持有了 object 对象的锁,但线程 B 完全可以 异步调用非 synchronized 类型的方法。 继续实验,将 MyObject.java 文件中的 methodB() 方法前加上 synchronized 关键字,代码如下: synchronized public void methodB() { try { System.out.println("begin methodB threadName=" + Thread.currentThread().getName() + " begin time=" + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("end"); 第 2 章 对象及变量的并发访问   63 } catch (InterruptedException e) { e.printStackTrace(); } } 本示例是两个线程访问同一个对象的两个同步的方法,运行结果如图 2-10 所示。   图 2-9 线程 B 异步调用非同步方法 图 2-10 同步运行 此实验的结论是: 1)A 线程先持有 object 对象的 Lock 锁,B 线程可以以异步的方式调用 object 对象中的 非 synchronized 类型的方法。 2)A 线程先持有 object 对象的 Lock 锁,B 线程如果在这时调用 object 对象中的 synchronized 类型的方法则需等待,也就是同步。 2.1.5 脏读 在 2.1.4 节示例中已经实现多个线程调用同一个方法时,为了避免数据出现交叉的情况, 使用 synchronized 关键字来进行同步。 虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是 脏读(dirtyRead)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。 创建 t3 项目,PublicVar.java 文件代码如下: package entity; public class PublicVar { public String username = "A"; public String password = "AA"; synchronized public void setValue(String username, String password) { try { this.username = username; Thread.sleep(5000); this.password = password; System.out.println("setValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); } catch (InterruptedException e) { e.printStackTrace(); } } public void getValue() { 64   Java 多线程编程核心技术 System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); } } 同步方法 setValue() 的锁属于类 PublicVar 的实例。 创建线程类 ThreadA.java 的代码如下: package extthread; import entity.PublicVar; public class ThreadA extends Thread { private PublicVar publicVar; public ThreadA(PublicVar publicVar) { super(); this.publicVar = publicVar; } @Override public void run() { super.run(); publicVar.setValue("B", "BB"); } } 文件 Test.java 代码如下: package test; import entity.PublicVar; import extthread.ThreadA; public class Test { public static void main(String[] args) { try { PublicVar publicVarRef = new PublicVar(); ThreadA thread = new ThreadA(publicVarRef); thread.start(); Thread.sleep(200);// 打印结果受此值大小影响 publicVarRef.getValue(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 程序运行后的结果如图 2-11 所示。 图 2-11 出现脏读情况 第 2 章 对象及变量的并发访问   65 出现脏读是因为 public void getValue() 方法并不是同步的,所以可以在任意时候进行调 用。解决办法当然就是加上同步 synchronized 关键字,代码如下: synchronized public void getValue() { System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); } 程序运行后的结果如图 2-12 所示。 图 2-12 不再出现脏读了 可见,方法 setValue() 和 getValue() 被依次执行。通过这个案例不仅要知道脏读是通过 synchronized 关键字解决的,还要知道如下内容: 当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时, A 线程就获得了 X 方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等 A 线程执行完毕才可以调用 X 方法,但 B 线程可以随意调用其他的非 synchronized 同步方法。 当 A 线程调用 anyObject 对象加入 synchronized 关键字的 X 方法时, A 线程就获得了 X 方法所在对象的锁,所以其他线程必须等 A 线程执行完毕才可以调用 X 方法,而 B 线程如 果调用声明了 synchronized 关键字的非 X 方法时,必须等 A 线程将 X 方法执行完,也就是 释放对象锁后才可以调用。这时 A 线程已经执行了一个完整的任务,也就是说 username 和 password 这两个实例变量已经同时被赋值,不存在脏读的基本环境。 脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。 2.1.6 synchronized 锁重入 关键字 synchronized 拥有锁重入的功能,也就是在使用 synchronized 时,当一个线程 得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个 synchronized 方法 / 块的内部调用本类的其他 synchronized 方法 / 块时,是永远可以得到锁的。 创建实验用的项目 synLockIn_1,类 Service.java 代码如下: package myservice; public class Service { synchronized public void service1() { System.out.println("service1"); service2(); } synchronized public void service2() { 66   Java 多线程编程核心技术 System.out.println("service2"); service3(); } synchronized public void service3() { System.out.println("service3"); } } 线程类 MyThread.java 代码如下: package extthread; import myservice.Service; public class MyThread extends Thread { @Override public void run() { Service service = new Service(); service.service1(); } } 运行类 Run.java 代码如下: package test; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); } } 程序运行结果如图 2-13 所示。 “可重入锁”的概念是:自己可以再次获取自己的内部锁。 比如有 1 条线程获得了某个对象的锁,此时这个对象锁还没 有释放,当其再次想要获取这个对象的锁的时候还是可以获 取的,如果不可锁重入的话,就会造成死锁。 可重入锁也支持在父子类继承的环境中。 创建实验用的项目 synLockIn_2,类 Main.java 代码如下: package myservice; public class Main { public int i = 10; synchronized public void operateIMainMethod() { try { i--; System.out.println("main print i=" + i); Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block 图 2-13 运行结果 第 2 章 对象及变量的并发访问   67 e.printStackTrace(); } } } 子类 Sub.java 代码如下: package myservice; public class Sub extends Main { synchronized public void operateISubMethod() { try { while (i > 0) { i--; System.out.println("sub print i=" + i); Thread.sleep(100); this.operateIMainMethod(); } } catch (InterruptedException e) { e.printStackTrace(); } } } 自定义线程类 MyThread.java 代码如下: package extthread; import myservice.Main; import myservice.Sub; public class MyThread extends Thread { @Override public void run() { Sub sub = new Sub(); sub.operateISubMethod(); } } 运行类 Run.java 代码如下: package test; import extthread.MyThread; public class Run { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); } } 程序运行后的效果如图 2-14 所示。 此实验说明,当存在父子类继承关系时,子类是完全可以通过 “可重入锁”调用父类的同步方法的。 图 2-14  重入到父类 中的锁 68   Java 多线程编程核心技术 2.1.7 出现异常,锁自动释放 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。 创建实验用的项目 throwExceptionNoLock,类 Service.java 代码如下: package service; public class Service { synchronized public void testMethod() { if (Thread.currentThread().getName().equals("a")) { System.out.println("ThreadName=" + Thread.currentThread().getName() + " run beginTime=" + System.currentTimeMillis()); int i = 1; while (i == 1) { if (("" + Math.random()).substring(0, 8).equals("0.123456")) { System.out.println("ThreadName=" + Thread.currentThread().getName() + " run exceptionTime=" + System.currentTimeMillis()); Integer.parseInt("a"); } } } else { System.out.println("Thread B run Time=" + System.currentTimeMillis()); } } } 两个自定义线程代码如图 2-15 所示。 图 2-15 两个线程类代码 运行类 Run.java 代码如下: 第 2 章 对象及变量的并发访问   69 package controller; import service.Service; import extthread.ThreadA; import extthread.ThreadB; public class Test { public static void main(String[] args) { try { Service service = new Service(); ThreadA a = new ThreadA(service); a.setName("a"); a.start(); Thread.sleep(500); ThreadB b = new ThreadB(service); b.setName("b"); b.start(); } catch (InterruptedException e) { e.printStackTrace(); } } } 程序运行后的效果如图 2-16 所示。 图 2-16 运行效果 线程 a 出现异常并释放锁,线程 b 进入方法正常打印,实验的结论就是出现异常的锁被 自动释放了。 2.1.8 同步不具有继承性 同步不可以继承。 创建测试用的项目 synNotExtends,类 Main.java 代码如下: package service; public class Main { synchronized public void serviceMethod() { try { System.out.println("int main 下一步 sleep begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); Thread.sleep(5000); 70   Java 多线程编程核心技术 System.out.println("int main 下一步 sleep end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } 类 Sub.java 代码如下: package service; public class Sub extends Main { @Override public void serviceMethod() { try { System.out.println("int sub 下一步 sleep begin threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); Thread.sleep(5000); System.out.println("int sub 下一步 sleep end threadName=" + Thread.currentThread().getName() + " time=" + System.currentTimeMillis()); super.serviceMethod(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } 类 MyThreadA.java 和 MyThreadB.java 代码如图 2-17 所示。 图 2-17 两个线程代码 类 Test.java 代码如下: 第 2 章 对象及变量的并发访问   71 package controller; import service.Sub; import extthread.MyThreadA; import extthread.MyThreadB; public class Test { public static void main(String[] args) { Sub subRef = new Sub(); MyThreadA a = new MyThreadA(subRef); a.setName("A"); a.start(); MyThreadB b = new MyThreadB(subRef); b.setName("B"); b.start(); } } 程序运行后的效果如图 2-18 所示。 图 2-18 运行效果 从此示例可以看到,同步不能继承,所以还得在子类的方法中添加 synchronized 关键 字,添加后的运行效果如图 2-19 所示。 图 2-19 同步了 2.2 synchronized 同步语句块 用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方 法执行一个长时间的任务,那么 B 线程则必须等待比较长时间。在这样的情况下可以使用 synchronized 同步语句块来解决。
还剩81页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 2 人已下载

下载pdf

pdf贡献者

翰雪飘香

贡献于2017-03-21

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf