第 1 章 Java 多线程技能
本章主要内容
线程的启动
如何使线程暂停
如何使线程停止
线程的优先级
线程安全相关的问题
1.1 进程和多线程的概念及线程的优点
进程是操作系统结构的基础;是一次程序的执行;是一个程序及其数据在处理机上顺序执行时所发生的活动;是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。进程是操作系统管理的基本运行单元。
那什么是线程呢?线程可以理解成是在进程中独立运行的子任务。
使用多线程技术后,可以在同一时间内运行更多不同种类的任务。
1.2 使用多线程
一个进程正在运行时至少会有 1 个线程在运行。
1.2.1 继承 Thread 类
实现多线程编程的方式主要有两种:一种是继承 Thread 类,另一种是实现 Runnable 接口。
Thread 类实现了 Runnable 接口,它们之间具有多态关系。
线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法。
Thread.java 类中的 start() 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的 run() 方法。这个过程其实就是让系统安排一个时间来调用 Thread 中的 run() 方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码 thread.run() 就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由 main 主线程来调用 run() 方法,也就是必须等 run() 方法中的代码执行完后才可以执行后面的代码。
执行 start() 方法的顺序不代表线程启动的顺序。
1.2.2 实现 Runnable 接口
使用继承 Thread 类的方式来开发多线程应用程序在设计上是有局限性的,因为 Java 是单根继承,不支持多继承,所以为了改变这种限制,可以使用实现 Runnable 接口的方式来实现多线程技术。 Thread.java 类也实现了 Runnbale 接口。
1.2.3 实例变量与线程安全
自定义线程类中的实例变量针对其他线程可以有共享与不共享之分。
每个线程都有各自的变量,自己改变自己变量的值,这种情况就是变量不共享。
共享数据的情况就是多个线程可以访问同一个变量。
非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
可以使用 synchronized 关键字解决非线程安全问题。
1.2.4 留意 i— 与 System.out.println() 的异常
System.out.println("i="+(i--)+" threadName="+Thread.currentThread().getName());
虽然 println() 方法在内部是同步的,但是 i— 的操作却是在进入 println() 之前发生的,所以有发生非线程安全问题的概率。所以,为了防止发生非线程安全问题,还是应继续使用同步方法。
1.3 currentThread() 方法
currentThread() 方法可返回代码段正在被哪个线程调用的信息。
1.4 isAlive() 方法
方法 isAlive() 的功能是判断当前的线程是否处于活动状态。什么是活动状态呢?活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备开始运行的状态,就认为线程是“存活”的。
1.5 sleep() 方法
方法 sleep() 的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指 this.currentThread() 返回的线程。
1.6 getId() 方法
getId() 方法的作用是取得线程的唯一标识。
1.7 停止线程
停止一个线程意味着在线程处理完任务之前停掉正在做的操作,也就是放弃当前的操作。停止一个线程可以使用 Thread.stop() 方法,但最好不用它。虽然它确实可以停止一个正在运行的线程,但是这个方法是不安全的(unsafe),而且是已被弃用作废的(deprecated),在将来的 Java 版本中,这个方法将不可用或不被支持。
大多数停止一个线程的操作使用 Thread.interrupt() 方法,尽管方法的名称是“停止,中止”的意思,但这个方法不会中止一个正在运行的线程,还需要加入一个判断才可以完成线程的停止。
在 Java 中有以下 3 种方法可以终止正在运行的线程:
1)使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止。
2)使用 stop 方法强行终止线程,但是不推荐使用这个方法,因为 stop 和 suspend 及 resume 一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
3)使用 interrupt 方法中断线程。
1.7.1 停止不了的线程
调用 interrupt() 方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。
1.7.2 判断线程是否是停止状态
在 Java 的 SDK 中,Thread.java 类里提供了两种方法来判断线程的状态是不是停止的。
1)this.interrupted():测试当前线程是否已经中断。
2)this.isInterrupted():测试线程是否已经中断。
interrupted() 方法的解释:测试当前线程是否已经中断。
官方文档对 interrupted 方法的解释:测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。interrupted() 方法具有清除状态的功能。
方法 isInterrupted() 并未清除状态标志。
this.interrupted():测试当前线程是否已经是中断状态,执行后具有将状态清除为 false 的功能。this.isInterrupted():测试线程 Thread 对象是否已经是中断状态,但不清除状态标志。
1.7.3 能停止的线程—-异常法
使用 throw new InterruptedException() 方法,在线程中断之后停止中断之后的代码运行。
1.7.4 在沉睡中停止
如果在 sleep 状态下停止某一线程,会进入 catch 语句,并且清除停止状态值,使之变成 false。
1.7.5 能停止的线程—-暴力停止
使用 stop() 方法停止线程则是非常暴力的。
1.7.6 方法 stop() 与 java.lang.ThreadDeath 异常
调用 stop() 方法时会抛出 java.lang.ThreadDeath 异常,但在通常的情况下,此一场不需要显示地捕获。
方法 stop() 已经被作废,因为如果强制让线程停止则有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了“解锁”,导致数据得不到同步的处理,出现数据不一致的问题。
1.7.7 释放锁的不良后果
使用 stop() 释放锁将会给数据造成不一致性的结果。如果出现这样的情况,程序处理的数据就有可能遭到破坏,最终导致程序执行的流程错误,一定要特别注意。
由于 stop() 方法已经在 JDK 中被标明使“作废/过期”的方法,显然它在功能上具有缺陷,所以不建议在程序中使用 stop() 方法。
1.7.8 使用 return 停止线程
将方法 interrupted() 与 return 结合使用也能实现停止线程的效果。
在线程中断之后,使用 return 使得无法运行中断之后的代码。
不过还是建议使用“抛异常”的方法来实现线程的停止,因为在 catch 块中还可以将异常向上抛,使线程停止的事件得以传播。
1.8 暂停线程
暂停线程意味着此线程还可以恢复运行。在 Java 多线程中,可以使用 suspend() 方法暂停线程,使用 resume() 方法恢复线程的执行。
1.8.1 suspend 与 resume 方法的使用
suspend() 方法会暂停线程,resume() 方法hi恢复线程的执行。
1.8.2 suspend 与 resume 方法的缺点—-独占
在使用 suspend 与 resume 方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。
1.8.3 suspend 与 resume 方法的缺点—-不同步
在使用 suspend 与 resume 方法时也容易出现因为线程的暂停而导致数据不同步的情况。
1.9 yield 方法
yield() 方法的作用使放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 执行事件。但放弃的时间不确定,有可能刚刚放弃,马上又获得 CPU 时间片。
1.10 线程的优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的 CPU 资源较多,也就是 CPU 优先执行优先级较高的线程对象中的任务。
设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
设置线程的优先级使用 setPriority() 方法。
在 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 是一样的。
1.10.2 优先级具有规则性
高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完。线程的哟先机与代码执行顺序无关,线程的优先级具有一定的规则性,也就是 CPU 尽量将执行资源让给优先级比较高的线程。
1.10.3 优先级具有随机性
线程的优先级还具有“随机性”,也就是优先级较高的线程不一定每一次都先执行完。
不要把线程的优先级与运行结果的顺序作为衡量的标准,优先级较高的线程并不一定每一次都先执行完 run() 方法中的任务,也就是说,线程优先级与打印顺序无关,不要将这两者的关系相关联,它们的关系具有不确定性和随机性。
1.10.4 看谁运行得快
优先级高的运行的快。
1.11 守护线程
在 Java 线程中有两种线程,一种是用户线程,另一种是守护现线程。
守护线程是一种特殊的线程,它的特性是“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进行中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。
setDaemon(true)将线程设置为守护线程。
1.12 本章小结
本章介绍了 Thread 类的 API,在使用这些 API 的过程中,会出现一些意想不到的情况,其实这也是多线程具有不可预知性的一个体现。