第 11章 线程线程的概念
程序是一段静态的代码,它是应用程序执行的蓝本
一个进程既包括其所要执行的指令,也包括了执行指令所需的任何系统资源,如 CPU、
内存空间,I/O端口等,不同进程所占用的系统资源相对独立
线程是进程执行过程中产生的多条执行线索,是比进程单位更小的执行单位线程的结构
CPU
Code Data
虚拟 CPU,封装在
java.lang.Thread类中,它控制着整个线程的运行执行的代码,传递给 Thread类,由
Thread类控制顺序执行 处理的数据,传递给Thread类,是在代码执行过程中所要处理的数据线程与进程
线程在形式上同进程十分相似 —都是用一个顺序执行的语句序列来完成特定的功能
不同之处:
线程没有入口,也没有出口,因此其自身不能自动运行,而必须栖身于某一进程之中,由进程触发执行
在系统资源的使用上,属于同一进程的所有线程共享该进程的系统资源,但是线程之间切换的速度比进程切换要快得多进程与线程的区别文件 输入输出装置各种系统资源数据区段程序区段只有一个地方在执行文件 输入输出装置各种系统资源数据区段程序区段同时有数个地方在执行传统的进程 多线程的任务多线程的优势
多线程编程简单,效率高。使用多线程可以在线程间直接共享数据和资源,而多进程之间不能做到这一点
适合于开发服务程序
如 Web服务、聊天服务等
适合于开发有多种交互接口的程序
如聊天程序的客户端、网络下载工具
适合于有人机交互又有计算量的程序
如字处理程序 Word,Excel等线程的状态
Java的线程是通过 Java的软件包 java.lang中定义的类 Thread来实现的
当生成一个 Thread类的对象之后,就产生了一个线程。通过该对象实例,可以启动线程、终止线程,或者暂时挂起线程等
Thread类本身只是线程的虚拟 CPU,线程所执行的代码是通过方法 run() 来完成的,方法 run()称为线程体
在一个线程被建立并初始化以后,Java的运行时系统就自动调用 run()方法,正是通过 run()方法才使得建立线程的目的得以实现线程的状态
线程一共有四种状态:新建( new),可运行状态( runnable),死亡( dead) 及堵塞
( blocked)
new Thread()
创建新线程 可运行态
start()
不可运行态
stop()
stop()
死亡
yield()
stop()
run()exit
suspend()
sleep()
wait()
I/O流阻塞
resume()
notify()/notify
All()
I/O指令等待睡眠挂起阻塞就绪运行新建
线程对象刚刚创建,还没有启动,此时还处于不可运行状态
Thread thread = new Thread(“test”)
此时线程 thread处于新建状态,但已有了相应的内存空间 以及其它资源可运行状态 runnable
此时的线程已经启动,处于线程的 run()方法之中。这种情况下线程可能正在运行,
也可能没有运行,只要 CPU一空闲,马上就会运行
调用线程的 start()方法可使线程处于,可运行,状态
thread.start();
死亡( dead)
线程死亡的原因:
run()方法中最后一个语句执行完毕
当线程处于,可运行,状态时,调用了 stop()方法结束了线程的运行,使其进入了死亡状态
thread.stop();
阻塞( blocked)
一个正在执行的线程因特殊原因,被暂停执行,就进入阻塞状态
阻塞时线程不能进入队列排队,必须等到引起阻塞的原因消除,才可重新进入排队队列
引起阻塞的原因很多,不同原因要用不同的方法解除
sleep()和 wait()是两个常用的引起阻塞的方法中断线程
interrupt()
向一个线程发送一个中断请求,同时把这个线程的,interrupted”状态置为 true。 若该线程处于
,blocked”状态,会抛出 InterruptedException
异常
static boolean interrupted()
检测当前线程是否已被中断,并重置状态
,interrupted”值为 false
boolean isInterrupted()
检测当前线程是否已被中断,不改变状态
,interrupted”值创建线程
类 Thread的构造方法如下
public Thread( ThreadGroup group,Runnable target,String name)
指明了线程所属的线程组是线程体 run()方法所在的对象,必须实现接口 Runnable
线程的名称创建线程的方法
创建线程的方法一 ——继承 Thread类
定义一个线程类,它继承类 Thread并重写其中的方法 run()。 在初始化这个类的实例时,目标对象 target可以为 null,表示这个实例本身具有线程体
创建线程的方法二 ——实现 Runnable接口
Runnable是 Java中用以实现线程的接口,从根本上讲,任何实现线程功能的类都必须实现该接口方法一
从 Thread类派生出一个子类,在类中一定要实现 run()
然后用该类创建一个对象
用 start()方法启动线程 (程序 11-1 11-2 )
class Lefthand extends Thread {
public void run(){
……
}
}
Lefthand left = new Lefthand();
left.start();
方法二
Thread的构造方法中包含有一个 Runnable实例的参数,必须定义一个实现 Runnable接口的类并产生一个该类的实例,对该实例的引用就是适合于这个构造方法的参数
class BallThread extends Applet implements Runnable{
public void start(){
thread=new Thread(this);
thread.start();
}
private Thread thread;
}
public class xyz implements Runnable{
int i;
public void run(){
w ile (true) {
System.out.println("Hello "+i++);
}
}
}
Runnable r = new xyz();
Thread t = new Thread(r);
CPU
Code Data
Thread t
xyz r
class xyz
线程模拟小球例子
程序 11-3是一个模拟小球平抛和自由落体的例子 BallThread.java
相应的 HTML文档两种方法的讨论
适用于采用实现 Runnable接口方法的情况
需要多继承的情况,比如对于 Applet程序
保持程序风格的一贯性
适用于采用继承 Thread方法的情况
为代码稍微简洁一些
在以后的继承中可能会出现麻烦线程的启动
通过 Thread类中方法 start()来启动在程序 11-3中,只要执行:
red.start();
blue.start();
线程的操作方法
start()
启动线程对象;
run()
用来定义线程对象被调度之后所执行的操作,用户必须重写 run()方法;
yield()
强制终止线程的执行;
isAlive()
测试当前线程是否在活动;
sleep(int millsecond)
使线程休眠一段时间,时间长短由参数所决定;
void Wait()
使线程处于等待状态;
线程的调度
线程调度通常是 抢占式,而不是 时间片式
抢占式调度是指可能有多个线程准备运行,但只有一个在真正运行。一个线程获得执行权,
这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,再或者有另一个高优先级线程就绪,最后一种情况中称为低优先级线程被高优先级线程所抢占。
优先级策略
优先级高的先执行,优先级低的后执行
多线程系统会自动为每个线程分配一个优先级,缺省时,继承其父类的优先级
任务紧急的线程,其优先级较高
同优先级的线程按,先进先出,的原则线程优先级
Thread类三个与线程优先级有关的静态量
Thread类中几个常用的有关优先级的方法
MAX_PRIORITY,最大优先权,值为 10;
MIN_PRIORITY,最小优先权,值为 1;
NORM_PRIORITY,默认优先权,值为 5
Void setPriority(int newPriority) //重置线程优先级
Int getPriority() //获得当前线程的优先级
Static void yield() //使当前线程放弃执行权线程的调度
被阻塞的线程按次序排列,组成一个阻塞队列。
所有就绪但没有运行的线程则根据其优先级排入一个就绪队列
CPU空闲时,如果就绪队列不空,队列中第一个具有最高优先级的线程将运行
当一个线程被抢占而停止运行时,它的运行态被改变并放到就绪队列的队尾;同样,一个被阻塞
(可能因为睡眠或等待 I/O设备)的线程就绪后通常也放到就绪队列的队尾
为保证给其他线程留有执行的机会,可以通过间隔地调用 sleep()
线程的调度例
例 11-4
public class xyz implements Runnable{
public void run(){
while(true){
…… // 执行若干操作
// 给其他线程运行的机会
try{
Thread.sleep(10);
}catch(InterruptedException e){
// 该线程为其他线程所中断
}
}
}
}
sleep()
是静态方法,可以通过 Thread.sleep(x)直接引用
x指定了线程在再次启动前必须休眠的最小时间,
毫秒为单位
可能引发中断异常 InterruptedException,因此要进行捕获和处理
,最小时间,是因为这个方法只保证在一段时间后线程回到就绪态,至于它是否能够获得 CPU
运行,则要视线程调度而定,所以,通常线程实际被暂停的时间都比指定的时间要长
yield()
可以给其他同等优先级线程一个运行的机会
如果在就绪队列中有其他同优先级的线程,
yield()把调用者放入就绪队列尾,并允许其他线程运行;如果没有这样的线程,则
yield()不做任何工作
sleep()调用允许低优先级进程运行,而
yield()方法只给同优先级进程以运行机会线程的基本控制
结束线程
当一个线程从 run()方法的结尾处返回时,它自动消亡并不能再被运行,可以将其理解为自然死亡
利用 stop()方法强制停止,可以将其理解为强迫死亡,这种方法必须用于 Thread类的特定实例中结束线程 例
例 11-5 强迫死亡
利用 Thread类中的静态方法 currentThread()
来引用正在运行的线程,见 例 11-6
在这个例子中,执行 stop()将破坏当前的运行环境,因而 run()中的循环在此情况下将不再运行检查线程
isAlive()
获取一个线程是否还在活动状态的信息。
活动状态不意味着这个线程正在执行,而只说明这个线程已被启动,并且既没有运行 stop(),
也尚未运行完方法 run()。
挂起线程
暂停一个线程称为挂起。在挂起之后,必须重新唤醒线程进入运行
挂起线程的方法
sleep()
线程不是休眠期满后就立刻被唤醒,因为此时其他线程能正在执行,重新调度只在以下几种情况下才会发生:
1) 被唤醒的线程具有更高的优先级;
2) 正在执行的线程因为其他原因被阻塞;
3) 程序处于支持时间片的系统中
suspend()和 resume()
join(),引起现行线程等待,直至方法 join所调用的线程结束程序 11-4
suspend()和 resume() 程序 11-4
说明:线程 t在运行到 suspend()以后被强制挂起,暂停运行,直到主线程调用 t.resume()
时才被重新唤醒 ;
一个线程只能被不同于它自身的线程所唤醒,因为在执行 suspend()方法以后,这个线程其后的代码都不会被执行到,而该线程又依赖于其后的 resume语句将其唤醒,所以千万不能将 resume()用在已被挂起的线程语句中
join() 例 11-7
已经生成并运行了一个线程 tt,而在另一线程中执行方法 timeout(),其定义如下,在执行方法 timeout()以后,现行的线程将被阻塞,直到 tt运行结束
public void timeout(){
// 暂停该线程,等候其他线程结束
tt.join();
// 其他线程结束后,继续执行该线程
…………
}
join()方法在调用时也可以使用一个以毫秒计的时间值,此时 join方法将挂起现行线程
timeout毫秒,或直到调用的线程结束,实际挂起时间以二者中时间较少的为准
void join(long timeout);
同步问题
线程间的通信
管道流可以连接两个线程间的通信
两个线程,一个写线程往管道流中输出信息,一个读线程从管道流中读入信息 程序
11-5
线程间资源互斥共享
通常,一些同时运行的线程需要共享数据,
每个线程就必须要考虑与他一起共享数据的其他线程的状态与行为,否则不能保证共享数据的一致性,从而不能保证程序的正确性。
例 11-8
一个代表栈的类 例 11-8
错误 情况 1
假设线程 a负责加入字符,线程 b负责移出字符。
线程 a刚刚加入了一个字符,例如是 r,但是尚未递增索引值,由于某种原因,恰恰这时它被抢占了,
buffer | p | q | r | | | |
idx=2 ^idx=3
错误 情况 2
如果此时线程 b正在等待移出一个字符,当线程 a处于等待状态时,线程 b就得到了运行机会。这样,在进入方法 pop()时,数据状态已经是错误的。 pop方法将继续递减索引值,idx变为 1
buffer | p | q | r | | | |
idx=1 ^ 操作后将返回字符
,q”,而忽略字符
,r”
错误 情况 3
如果线程 a从 push()方法中被打断的地方继续运行,递增了索引值,idx修正为 2,此时的状态下,q”是有效的,而包含了,r”的位置是下一个空元素。再出栈的话,又会得到,q”。 换句话说,我们将再次读到,q”,
就好像它被存入栈中两次一样,但是将再也读不到字母,r”了
buffer | p | q | r | | | |
inx=2 ^
例子 说明
产生这种问题的原因是对共享资源访问的不完整性
完整性称为共享数据操作的同步,共享数据叫做条件变量
解决问题的方法
禁止线程在完成代码关键部分时被切换
提供一个特殊的锁定标志来处理数据 (Java采用的方法 )
对象的锁定标志
,对象互斥锁,(又称为监视器、管程)
实现不同线程对共享数据操作的同步。,对象互斥锁,阻止多个线程同时访问同一个条件变量。 Java可以为每一个对象的实例配有一个
,对象互斥锁,
实现,对象互斥锁,两种方法
用关键字 volatile来声明一个共享数据(变量);
用关键字 synchronized来声明一个操作共享数据的方法或一段代码。
例 11-9
class stack{
int idx = 0;
char data[] = new char[6];
public void push(char c){
synchronized (this){
data[idx] = c;
idx ++;
}
}
…………
public char pop(){
synchronized (this){
idx --;
return data[idx];
}
}
}
增加了一个对
synchronized(this)
的调用增加了一个对
synchronized(this)
的调用过程图 示线程 1
...
因等待同步资源而挂起的线程队列同步方法
用 synchronized来标识的代码段或方法即为
“对象互斥锁”锁住的部分。如果一个程序内有两个或以上的方法使用 synchronized
标志,则它们在同一个“对象互斥锁”管理之下
一般情况下,都使用 synchronized关键字在方法的层次上实现对共享资源操作的同步,
很少使用 volatile关键字声明共享变量
synchronized()语句写法
标准写法 (更为妥帖 )
简洁的写法
public void push(char c){
synchronized(this){
…………
}
}
public synchronized void push(char c){

}
把 synchronized用做方法的修饰字,则整个方法都将视作同步块,这可能会使持有锁定标记的时间比实际需要的时间要长,
从而降低效率死锁
死锁情况发生在一个线程等待另一个线程所持有的锁,而第二个线程又在等待第一个线程持有的锁的时候。每个线程都不能继续运行,除非另一线程运行完同步程序块。而恰恰因为哪个线程都不能继续运行,
所以哪个线程都无法运行完同步程序块线程 2
pen
线程 1
note
把,pen”给我,我才能给你,note”
把,note”给我,我才能给你,pen”
死锁问题程序
死锁问题见 程序 11-6
解决死锁问题的方法
给资源施加排序
制定一个规则来决定以何种顺序来获得这些锁,并在整个程序中遵循这个顺序
参考操作系统方面的相关书籍线程交互
为什么两个线程需要交互呢?
涉及到多线程间共享数据操作时,除了同步问题之外,还会遇到如何控制相互交互的线程之间的运行进度,即多线程的同步问题
生产者 ——消费者问题
生产者比消费者快时,消费者漏掉一些数据
消费者比生产者快时,消费者取相同的数据生产者 消费者共享对象
put get
解决方法
Java中的每个对象实例都有两个线程队列和它相连。
第一个用来排列等待锁定标志的线程。
第二个则用来实现 wait()和 notify()的交互机制。
交互机制
wait()方法
让当前线程释放其所持有的“对象互斥锁”,进入 wait
队列
方法 wait()既可以被 notify()终止,也可以通过 interrupt()
方法来终止
notify()/notifyAll()方法
唤醒一个或所有正在等待队列中等待的线程,并将它
(们)移入等待同一个“对象互斥锁”的队列
notify()/notifyAll()方法和 wait ()方法只能在被声明为 synchronized的方法或代码段中调用状态以及状态间转换
Blocked
DeadNewborn
Runnable Running
stop()
start() stop()
yield()
stop()
stop()
resume()
notify()
suspend()
sleep()
wait()
守护线程
在客户 /服务器模式下,服务器的作用是持续等待用户发来的请求,并按请求完成客户的工作。此时用到 守护线程( Daemon)
守护线程是为其它线程提供服务的线程,
它一般应该是一个独立的线程,它的 run()
方法是一个无限循环。
守护线程是唯一运行着的线程时,程序会自动退出守护线程客户端 服务器端
request
daemon
守护线程相关方法
public boolean isDaemon()
确定一个线程是否是守护线程
public void setDaemon( boolean )
调用 setDaemon(true)方法,可以使线程成为守护线程(必须在 start之前调用)
调用 setDaemon(false)方法,可以使线程成为一般线程(必须在 start之前调用)
综合应用
经典的生产者 /消费者问题的实际例子
生产者将随机产生 20个大写字母并将它们推入栈中,每次推入动作之间有一个随机的延时,
延时的范围为 0~ 100毫秒。每个推入的字母将在屏幕上显示 程序 11-7
消费者从栈中取出 20个字母,每次取出动作之间有延时,这里的延时为 0~ 2秒。这意味着栈的清空比填入要慢,因此栈能够很快被完全填满 程序 11-8
综合应用
栈类的构造
需要一个索引值和一个缓冲区数组,一个新构造的 SyncStack应该为空。
最终的程序代码 程序 11-12