Java程序设计精讲十一 第十章 多线程1
1 线程和多线程
1.1. 线程的概念
线程∶线程是进程执行过程中产生的多条执行线索,是比进程单位更小的执行单位,在形式上同进程十分相似——都是用一个按序执行的语句序列来完成特定的功能。线程必须栖身于某一个进程之中,由进程触发执行。在系统资源的使用上,属于同一进程的所有线程共享该进程的系统资源,但是线程之间切换的速度比进程切换要快得多。
多线程︰在单CPU的计算机内部,从微观上讲,一个时刻只能有一个作业被执行。实现多线程就是要在宏观上使多个作业被同时执行。多线程可以使系统资源特别是CPU的利用率得到提高,整个程序的执行效率也可以得到提高。
Java类库中的类`java. lang.Thread`允许创建的线程,可达到多线程的效果。
1.2. 线程的结构
1.2.1. 线程的组成
- 虚拟CPU,封装在java.lang.Thread类中,它控制着整个线程的运行。
- 执行的代码,传递给Thread类,由Thread类控制按序执行。
- 处理的数据,传递给Thread类,是在代码执行过程中所要处理的数据。
1.2.2. 当一个线程被构造时,它由构造方法参数、执行代码、操作数据来初始化。
1.2.3. 优点
多线程编程简单、效率高。
1.3. 线程的状态
在一个线程被建立并初始化以后,Java运行时系统自动调用run()方法,建立线程的目的得以实现。
线程一共有4种状态:新建(New)、可运行状态(Runnable)、死亡(Dead )及阻塞(Blocked )
1.3.1. 新建
线程对象刚刚创建,还没有启动,此时还处于不可运行状态。此时刚创建的线程处于新建状态,但已有了相应的内存空间以及其他资源。
1.3.2. 可运行状态
此时的线程已经启动,处于线程的run()方法之中。这种情况下线程可能∶
- 正在运行:处于运行状态。
- 没有运行︰处于就绪状态,只要CPU—空闲,马上就会运行。
调用线程的start()方法可使线程处于可运行状态。
1.3.3. 死亡
线程死亡的原因:
- run()方法中最后一个语句执行完毕
- 当线程遇到异常退出时便进入了死亡状态。
1.3.4. 阻塞
一个正在执行的线程因特殊原因,被暂停执行,就进入阻塞状态。阻塞时线程不能进入就绪队列排队,必须等到引起阻塞的原因消除,才可重新进入队列排队。
sleep()和wait()是两个常用的引起阻塞的方法。
1.3.5. 中断线程
在程序中常常调用interrupt()来终止线程。
测试线程是否被中断的方法:
- void interrupt()∶向一个线程发送一个中断请求,同时把这个线程的”interrupted”状态置为true。若该线程处于“blocked”状态,会抛出lnterruptedException异常。
- static boolean interrupted ()︰检测当前线程是否已被中断,并重置状态” interrupted”值。即如果连续两次调用该方法,则第二次调用将返回false.
- boolean isInterrupted()∶检测当前线程是否已被中断,不改变状态”interrupted”值。
2 创建线程
创建线程的方法∶
- 定义一个继承Thread类的子类。
- 实现Runnable接口。
2.1. 继承Thread类创建线程
2.1.1. Thread类典型的构造方法︰
1
2
3
4
5
6
/**
* @param name 作为新线程的名称,且是线程组group中的一员
* @param target 必须实现Runnable接口,当本线程启动时,将调用target的run( )方法
* 当target为null时,启动本线程的run()方法
*/
Thread(ThreadGroup group, Runnable target, String name);
用Thread类的子类创建线程 示例:
1
2
3
4
5
6
7
class Lefthand extends Thread{ //)从Thread类派生出一个子类,在类中一定要实现run()
public void run( ){
//线程体
}
}
Lefthand left = new Lefthand(); //然后用该类创建一个对象
left. start( ); //用start()方法启动线程
2.2. 实现Runable接口创建线程
2.2.1. 构造方法
Runnable接口中只定义了一个方法就是run( )方法,也就是线程体。用Runnable接口实现多线程时,也必须实现run( )方法,也需要使用start()启动线程,但此时常用Thread类的构造方法来创建线程对象。
Thread的构造方法中包含有一个Runnable实例的参数。
实现Runnable接口创建线程 示例:
1
2
3
4
5
6
7
8
9
class xyz implements Runnable {
@Override
public void run() {
//线程体
}
}
Thread t = new Thread(new xyz());
t.start();
2.3. 创建线程两种方法的适用条件
2.3.1. 适用于采用实现Runnable接口方法的情况
- 如果一个类已经继承了Thread,就不能再继承其他类,在一些情况下,这就被迫采用实现Runnable的方法。
- 由于原来的线程采用的是实现Runnable接口的方法,可能会出于保持程序风格的一贯性而继续使用这种方法。
2.3.2. 适用于采用继承Thread方法的情况
代码稍微简洁,所以许多Java程序员愿意使用继承Thread的方法。
例如︰ 当一个run()方法置于Thread类的子类中时,this实际上引用的是控制当前运行系统的Thread实例,所以,代码不必写得像下面这样烦琐。
Thread.currentThread(). getState();
而可简单地写为:getState();
3 线程的基本控制
3.1. 线程的启动
通过Thread类中的start()方法来启动。
API中提供的有关线程的操作方法︰
- start()
:启动线程对象,让线程从新建状态转为就绪状态。
- run()
:用来定义线程对象被调度之后所执行的操作,用户必须重写run()方法。
- yield()
:强制终止线程的执行,当前线程放弃执行权。
- isAlive()
:测试当前线程是否在活动。
- sleep(int millsecond)
:使线程休眠一段时间,时间长短由millsecond决定,单位为ms。
- void wait()
:使线程处于等待状态。
3.2. 线程的调度
3.2.1. 概念
线程调度通常是抢占式:一个线程获得执行权,这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,再或者有另一个高优先级线程就绪,最后一种情况称为低优先级线程被高优先级线程所抢占。
3.2.2. 优先级策略
- 优先级高的先执行,优先级低的后执行
。
- 每个线程创建时都会被自动分配一个优先级,默认时,继承其父类的优先级。
- 任务紧急的线程,其优先级较高。
- 同优先级的线程按“先进先出”的调度原则。
3.2.3. Thread类中与线程优先级有关的静态量
- MAX_PRIORITY:最高优先级,值为10。
- MIN_PRIORITY:最低优先级,值为1。
- NORM_PRIORITY:默认优先级,值为5。
3.2.4. java.lang.Thread类中有关优先级的几个常用方法
- void setPriority(int newPriority):重置线程优先级。
- int getPriority():获得当前线程的优先级。
- static void yield():暂停当前正在执行的线程,即让当前线程放弃执行权。
一个线程被阻塞的原因是多种多样的,可能是因为执行了Thread.sleep()调用,故意让它暂停一段时间;也可能是因为需要等待一个较慢的外部设备,例如磁盘或用户操作的键盘。所有被阻塞的线程按次序排列,组成一个阻塞队列。而所有就绪但没有运行的线程则根据其优先级进入一个就绪队列。
当CPU 空闲时,如果就绪队列不空,队列中第一个具有最高优先级的线程将运行。当一个线程被抢占而停止运行时,它的运行状态被改变并放到就绪队列的队尾;同样,一个被阻塞(可能因为睡眠或等待VO设备)的线程就绪后通常也放到就绪队列的队尾。
由于Java线程调度不是时间片式,所以在程序设计时要合理安排不同线程之间的运行顺序,以保证给其他线程留有执行的机会。为此,可以通过间隔地调用sleep()做到这一点。
3.2.5. 合理安排不同线程之间的运行顺序
- sleep()
调用允许低优先级进穆运行。
- yield()
方法只给同优先级进程以运行机会。
3.3. 结束线程
自动消亡:当一个线程从run()方法的结尾处返回时。
强迫死亡:遇到异常使得线程结束。
使用interrupt ()方法中断线程的执行。
用Thread类中的静态方法currentThread( )来引用正在运行的线程。
在程序代码中,可以利用Thread类中的静态方法currentThread()来引用正在运行的线程。有时候可能不知道一个线程的运行状态,这时可以使用方法 isAlive()来获取一个线程是否还在活动状态的信息。活动状态不意味着这个线程正在执行,而只说明这个线程已被启动。
3.4. 挂起线程
挂起︰暂停一个线程。在挂起之后,必须重新唤醒线程进入运行状态
3.4.1. 挂起线程的方法︰
- sleep() sleep()方法用于暂时停止一个线程的执行
。通常,线程不是休眠期满后就立刻被唤醒,因为此时其他线程可能正在执行。
重新调度只在以下几种情况下才会发生
- 被唤醒的线程具有更高的优先级。
- 正在执行的线程因为其他原因被阻塞。
- 程序处于支持时间片的系统中。
大多数情况下,后两种条件不会立刻发生。
- wait()和notify()/notifyAll() wait()方法导致当前的线程等待,直到其他线程调用此对象的notify( )方法或notifyAll()方法,才能唤醒线程
。
join() join()方法将引起现行线程等务,直至join()方法所调用的线程结束。join()方法在调用时也可以使用一个以毫秒计的时间值:
1
void join (long timeout);
此时join()方法将挂起现行线程timeout毫秒,或直到调用的线程结束,实际挂起时间以二者中时间较少的为准。
4 线程的互斥
4.1. 互斥问题的提出
通常,一些同时运行的线程需要共享数据。此时,每个线程就必须要考虑与它一起共享数据的其他线程的状态与行为,否则的话就不能保证共享数据的一致性,因而也就不能保证程序的正确性。
下面设计一个代表栈的类。这个类没有采取措施处理溢出,栈的能力也很有限。
4.2. 对象的锁定标志
4.2.1. 概念
“对象互斥锁”也称为监视器,使用它来实现不同线程对共享数据操作的同步。
4.2.2. 实现“对象互斥锁”的方法
- 用关键字volatile来声明一个共享数据(变量)。
- 用关键字synchronized来声明操作共享数据的一个方法或一段代码。用synchronized标识的代码段或方法即为“对象互斥锁”锁住的部分。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 栈示例
*/
class Stack {
int idx = 0;
char[] data = new char[6];
public void push(char c) {
synchronized (this) { //增加同步标志
data[idx] = c;
idx++;
}
}
public synchronized char pop() { //增加同步标志
idx--;
return data[idx];
}
}
现在pop()和 push()操作的部分增加了一个对synchronized( this)的调用,在第一个线程拥有锁定标记时,如果另一个线程企图执行synchronized( this)中的语句时,它将从对象 this中索取锁定标记。因为这个标记不可得,故该线程不能继续执行。实际上这个线程将加入等待队列,该等待队列与对象锁定标志相连,当标志被返还给对象时,等待标志的第一个线程将得到该标志并继续运行。
因为等待一个对象的锁定标志的线程要等到持有该标志的线程将其返还后才能继续运行,所以在不使用该标志时将其返还就显得十分重要了。事实上,当持有锁定标志的线程运行完synchronized()调用包含的程序块后,这个标志将会被自动返还。Java保证了该标志通常能够被正确地返还,即使被同步的程序块产生了一个异常,或者某个循环中断跳出了该程序块,这个标志也能被正确返还。同样,如果一个线程两次调用了同一个对象,在退出最外层后这个标志也将被正确释放,而在退出内层时则不会执行释放。
用synchronized标识的代码段或方法即为“对象互斥锁”锁住的部分。如果一个程序内有两个或以上的方法使用synchronized标志,则它们在同一个“对象互斥锁”管理之下。
4.2.3. synchronized语法
一般情况下,使用synchronized关键字在方法的层次上实现对共享资源操作的同步,很少使用volatile关键字声明共享变量。
1
2
3
4
5
6
7
8
9
//synchronized()语句的标准写法为:
public void push( char c){
synchronized( this){
//...
}
}
//由于synchronized()语句的参数必须是this,因此,Java语言允许使用下面这种简洁的写法:
public synchronized void push(char c){
}
比较以上两种写法,可以看出,前一种比后一种更为妥帖。如果把synchronized用作方法的修饰字,则整个方法都将视作同步块,这可能会使持有锁定标记的时间比实际需要的时间要长,从而降低效率。另一方面,使用前一种方法来做标记可以提醒用户同步在发生。
5 线程的同步
为了完成多个任务,常创建多个线程,它们可能毫不相关,但有时它们]完成的任务在某种程度上有一定的关系,此时就需要线程之间有一些交互。在Java中,使用一对方法wait( )和notify( )/ notifyall( )实现线程的交互。
5.1. 同步问题的提出
操作系统中的生产者消费者问题,就是一个经典的同步问题。
举一个简单的例子, 有两个人,一个人在刷盘子,另一个人在烘干。这两个人各自代表一个线程,他们之间有一个共享的对象一盘架,刷好而等待烘干的盘子放在盘架上。两个人在没有事情做时都愿意歇着。显然,盘架上有刷好的盘子时,烘干的人才能开始工作;而如果刷盘子的人刷得太快,刷好的盘子占满了盘架时,他就不能再继续工作了,而要等到盘架上有空位置才行。
这个示例要说明的问题是,生产者生产一个产品后就放人共享对象中,而不管共享对象 中是否已有产品。消费者从共享对象中取用产品,但不检测是否已经取过。
若共享对象中只能存放一个数据,可能出现以下问题:
- 生产者比消费者快时,消费者会漏掉一些数据没有取到。
- 消费者比生产者快时,消费者取相同的数据。
在Java语言中,可以用wait( )和notify( )/notifyAll( )方法来协调线程间的运行速度关系,这些方法都定义在java. lang. Object 类中。
5.2. 解决方法
为了解决线程运行速度问题,Java提供了一种建立在对象实例之上的交互方法。Java中的每个对象实例都有两个线程队列和它相连。第一个用来排列等待锁定标志的线程。第二个则用来实现wait()和 notify()的交互机制。
类java.lang. Object中定义了3个方法,即wait()、notify()和notifyAll()。notify( )/notifyAll()方法和wait()方法都只能在被声明为synchronized的方法或代码段中调用。
wait()方法导致当前的线程等待,它的作用是让当前线程释放其所持有的“对象互斥锁”,进入wait队列(等待队列) ;
notify()/notifyAll()方法的作用是唤醒 一个/所有 正在等待队列中等待的线程,并将它(们)移入等待同一个“对象互斥锁”的队列。
- notify()最多只能释放等待队列中的第一个线程,如果有多个线程在等待,则其他的线程将继续留在队列中。
- notifyAll()方法能够释放所有等待线程。
注意:
- 在调用一个对象的wait()、notify()/notifyAll()时,必须首先持有该对象的锁定标志,因此这些方法必须在同步程序块中调用。
- 在实际实现中,wait()方法既可以被notify()终止,又可以通过调用线程的interrupt()方法来终止。后一种情况下,wait()会抛出一个InterruptedException异常,所以需要把它放在try/catch结构中。