第13章 线程.docx
- 文档编号:12065176
- 上传时间:2023-04-16
- 格式:DOCX
- 页数:36
- 大小:56.24KB
第13章 线程.docx
《第13章 线程.docx》由会员分享,可在线阅读,更多相关《第13章 线程.docx(36页珍藏版)》请在冰豆网上搜索。
第13章线程
第13章线程
本模块讨论多线程,它允许一个程序同时执行多个任务。
第一节相关问题
讨论-以下为与本模块内容有关的问题:
●我如何使我的程序执行多个任务?
第二节目标
在完成了本模块的学习后,你应当能够:
●定义一个线程
●在一个Java程序中创建若干分离的线程,控制线程使用的代码和数据
●控制线程的执行,并用线程编写独立于平台的代码
●描述在多个线程共享数据时可能会碰到的困难
●使用synchronized关键字保护数据不受破坏
●使用wait()和notify()使线程间相互通信
●解释为什么在JDK1.2中不赞成使用suspend()、resume()和stop()方法?
第三节线程
线程
●什么是线程?
●虚拟处理机
13.3.1什么是线程?
一个关于计算机的简化的视图是:
它有一个执行计算的处理机、包含处理机所执行的程序的ROM(只读存储器)、包含程序所要操作的数据的RAM(只读存储器)。
在这个简化视图中,只能执行一个作业。
一个关于最现代计算机比较完整的视图允许计算机在同时执行一个以上的作业。
你不需关心这一点是如何实现的,只需从编程的角度考虑就可以了。
如果你要执行一个以上的作业,这类似有一台以上的计算机。
在这个模型中,线程或执行上下文,被认为是带有自己的程序代码和数据的虚拟处理机的封装。
java.lang.Thread类允许用户创建并控制他们的线程。
注-在这个模块中,使用“Thread”时是指java.lang.Thread而使用“thread”时是指执行上下文。
13.3.2线程的三个部分
线程的三个部分
●处理机
●代码
●数据
进程是正在执行的程序。
一个或更多的线程构成了一个进程。
一个线程或执行上下文由三个主要部分组成
●一个虚拟处理机
●CPU执行的代码
●代码操作的数据
代码可以或不可以由多个线程共享,这和数据是独立的。
两个线程如果执行同一个类的实例代码,则它们可以共享相同的代码。
类似地,数据可以或不可以由多个线程共享,这和代码是独立的。
两个线程如果共享对一个公共对象的存取,则它们可以共享相同的数据。
在Java编程中,虚拟处理机封装在Thread类的一个实例里。
构造线程时,定义其上下文的代码和数据是由传递给它的构造函数的对象指定的。
第四节Java编程中的线程
13.4.1创建线程
创建线程
●多线程编程
●从同一个Runnbale实例派生的多线程
●线程共享数据和代码。
本节介绍了如何创建线程,以及如何使用构造函数参数来为一个线程提供运行时的数据和代码。
一个Thread类构造函数带有一个参数,它是Runnable的一个实例。
一个Runnable是由一个实现了Runnable接口(即,提供了一个publicvoidrun()方法)的类产生的。
例如:
1.publicclassThreadTest{
2.publicstaticvoidmain(Stringargs[]){
3.Xyzr=newXyz();
4.Threadt=newThread(r);
5.}
6.}
7.
8.classXyzimplementsRunnable{
9.inti;
10.
11.publicvoidrun(){
12.while(true){
13.System.out.println("Hello"+i++);
14.if(i==50)break;
15.}
16.}
17.}
首先,main()方法构造了Xyz类的一个实例r。
实例r有它自己的数据,在这里就是整数i。
因为实例r是传给Thread的类构造函数的,所以r的整数i就是线程运行时刻所操作的数据。
线程总是从它所装载的Runnable实例(在本例中,这个实例就是r。
)的run()方法开始运行。
一个多线程编程环境允许创建基于同一个Runnable实例的多个线程。
这可以通过以下方法来做到:
Threadt1=newThread(r);
Threadt2=newThread(r);
此时,这两个线程共享数据和代码。
总之,线程通过Thread对象的一个实例引用。
线程从装入的Runnble实例的run()方法开始执行。
线程操作的数据从传递给Thread构造函数的Runnable的特定实例处获得。
13.4.2启动线程
启动线程
●使用start()方法
●使线程置于可运行状态
一个新创建的线程并不自动开始运行。
你必须调用它的start()方法。
例如,你可以发现上例中第4行代码中的命令:
t.start();
调用start()方法使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。
这并不意味着线程就会立即运行。
13.4.3线程调度
一个Thread对象在它的生命周期中会处于各种不同的状态。
下图形象地说明了这点:
尽管线程变为可运行的,但它并不立即开始运行。
在一个只带有一个
处理机的机器上,在一个时刻只能进行一个动作。
下节描述了如果有一个以上可运行线程时,如何分配处理机。
在Java中,线程是抢占式的,但并不一定是分时的(一个常见的错误是认为“抢占式”只不过是“分时”的一种新奇的称呼而已)。
抢占式调度模型是指可能有多个线程是可运行的,但只有一个线程在实际运行。
这个线程会一直运行,直至它不再是可运行的,或者另一个具有更高优先级的线程成为可运行的。
对于后面一种情形,低优先级线程被高优先级线程抢占了运行的机会。
一个线程可能因为各种原因而不再是可运行的。
线程的代码可能执行了一个Thread.sleep()调用,要求这个线程暂停一段固定的时间。
这个线程可能在等待访问某个资源,而且在这个资源可访问之前,这个线程无法继续运行。
所有可运行线程根据优先级保存在池中。
当一个被阻塞的线程变成可运行时,它会被放回相应的可运行池。
优先级最高的非空池中的线程会得到处理机时间(被运行)。
因为Java线程不一定是分时的,所有你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。
这可以通过在各种时间间隔中发出sleep()调用来做到。
1.publicclassXyzimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//dolotsofinterestingstuff
5.:
6.//Giveotherthreadsachance
7.try{
8.Thread.sleep(10);
9.}catch(InterruptedExceptione){
10.//Thisthread'ssleepwasinterrupted
11.//byanotherthread
12.}
13.}
14.}
15.}
注意try和catch块的使用。
Thread.sleep()和其它使线程暂停一段时间的方法是可中断的。
线程可以调用另外一个线程的interrupt()方法,这将向暂停的线程发出一个InterruptedException。
注意Thread类的sleep()方法对当前线程操作,因此被称作Thread.sleep(x),它是一个静态方法。
sleep()的参数指定以毫秒为单位的线程最小休眠时间。
除非线程因为中断而提早恢复执行,否则它不会在这段时间之前恢复执行。
Thread类的另一个方法yield(),可以用来使具有相同优先级的线程获得执行的机会。
如果具有相同优先级的其它线程是可运行的,yield()将把调用线程放到可运行池中并使另一个线程运行。
如果没有相同优先级的可运行进程,yield()什么都不做。
注意sleep()调用会给较低优先级线程一个运行的机会。
yield()方法只会给相同优先级线程一个执行的机会。
第五节线程的基本控制
13.5.1终止一个线程
当一个线程结束运行并终止时,它就不能再运行了。
可以用一个指示run()方法必须退出的标志来停止一个线程。
1.publicclassXyzimplementsRunnable{
2.privatebooleantimeToQuit=false;
3.
4.publicvoidrun(){
5.while(!
timeToQuit){
6....
7.}
8.//cleanupbeforerun()ends
9.}
10.
11.publicvoidstopRunning(){
12.timeToQuit=true;
13.}
14.}
15.
16.publicclassControlThread{
17.privateRunnabler=newXyz();
18.privateThreadt=newThread(r);
19.
20.publicvoidstartThread(){
21.t.start();
22.}
23.
24.publicvoidstopThread(){
25.//usespecificinstanceofXyz
26.r.stopRunning();
27.}
28.}
在一段特定的代码中,可以使用静态Thread方法currentThread()来获取对当前线程的引用,例如:
1.publicclassXyzimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//lotsofinterestingstuff
5.//Printnameofthecurrentthread
6.System.out.println("Thread"+
7.Thread.currentThread().getName()+
8."completed");
9.}
10.}
11.}
13.5.2测试一个线程
●测试一个线程
●isAlive()
●sleep()
●join()
有时线程可处于一个未知的状态。
isAlive()方法用来确定一个线程是否仍是活的。
活着的线程并不意味着线程正在运行;对于一个已开始运行但还没有完成任务的线程,这个方法返回true。
13.5.3延迟线程
存在可以使线程暂停执行的机制。
也可以恢复运行,就好象什么也每发生过一样,线程看上去就象在很慢地执行一条指令。
sleep()
sleep()方法是使线程停止一段时间的方法。
在sleep时间间隔期满后,线程不一定立即恢复执行。
这是因为在那个时刻,其它线程可能正在运行而且没有被调度为放弃执行,除非
(a)“醒来”的线程具有更高的优先级
(b)正在运行的线程因为其它原因而阻塞
1.publicclassXyzimplementsRunnable{
2.publicvoidrun(){
3.while(true){
4.//lotsofinterestingstuff
5.//Printnameofthecurrentthread
6.System.out.println("Thread"+
7.Thread.currentThread().getName()+
8."completed");
9.}
10.}
11.}
join()
join()方法使当前线程停下来等待,直至另一个调用join方法的线程终止。
例如:
publicvoiddoTask(){
TimerThreadtt=newTimerThread(100);
tt.start();
...
//Dostuffinparallelwiththeotherthreadfor
//awhile
...
//Waithereforthetimerthreadtofinish
try{
tt.join();
}catch(InterruptedExceptione){
//ttcamebackearly
}
...
//Nowcontinueinthisthread
...
}
可以带有一个以毫秒为单位的时间值来调用join方法,例如:
voidjoin(longtimeout);
其中join()方法会挂起当前线程。
挂起的时间或者为timeout毫秒,或者挂起当前线程直至它所调用的线程终止。
第六节创建线程的其它方法
到目前为止,你已经知道如何用实现了Runnable的分离类来创建线程上下文。
事实上,这不是唯一的方法。
Thread类自身实现了Runnable接口,所以可以通过扩展Thread类而不是实现Runnable来创建线程。
1.publicclassMyThreadextendsThread{
2.publicvoidrun(){
3.while(running){
4.//dolotsofinterestingstuff
5.try{
6.sleep(100);
7.}catch(InterruptedExceptione){
8.//sleepinterrupted
9.}
10.}
11.}
12.
13.publicstaticvoidmain(Stringargs[]){
14.Threadt=newMyThread();
15.t.start();
16.}
17.}
13.6.1使用那种方法?
使用那种方法?
●实现Runnable
●更符合面向对象的设计
●单继承
●一致性
●扩展Thread
●代码更简单
给定各种方法的选择,你如何决定使用哪个?
每种方法都有若干优点。
实现Runnable的优点
●从面向对象的角度来看,Thread类是一个虚拟处理机严格的封装,因此只有当处理机模型修改或扩展时,才应该继承类。
正因为这个原因和区别一个正在运行的线程的处理机、代码和数据部分的意义,本教程采用了这种方法。
●由于Java技术只允许单一继承,所以如果你已经继承了Thread,你就不能再继承其它任何类,例如Applet。
在某些情况下,这会使你只能采用实现Runnable的方法。
●因为有时你必须实现Runnable,所以你可能喜欢保持一致,并总是使用这种方法。
继承Thread的优点
●当一个run()方法体现在继承Thread类的类中,用this指向实际控制运行的Thread实例。
因此,代码不再需要使用如下控制:
Thread.currentThread().join();
而可以简单地用:
join();
因为代码简单了一些,许多Java编程语言的程序员使用扩展Thread的机制。
注意:
如果你采用这种方法,在你的代码生命周期的后期,单继承模型可能会给你带来困难。
第七节使用Java技术中的synchronized
本节讨论关键字synchronized的使用。
它提供Java编程语言一种机制,允许程序员控制共享数据的线程。
13.7.1问题
想象一个表示栈的类。
这个类最初可能象下面那样:
1.publicclassMyStack{
2.
3.intidx=0;
4.char[]data=newchar[6];
5.
6.publicvoidpush(charc){
7.data[idx]=c;
8.idx++;
9.}
10.
11.publiccharpop(){
12.idx--;
13.returndata[idx];
14.}
15.}
注意这个类没有处理栈的上溢和下溢,所以栈的容量是相当有限的。
这些方面和本讨论无关。
这个模型的行为要求索引值包含栈中下一个空单元的数组下标。
“先进后出”方法用来产生这个信息。
现在想象两个线程都有对这个类里的一个单一实例的引用。
一个线程将数据推入栈,而另一个线程,或多或少独立地,将数据弹出栈。
通常看来,数据将会正确地被加入或移走。
然而,这存在着潜在的问题。
假设线程a正在添加字符,而线程b正在移走字符。
线程a已经放入了一个字符,但还没有使下标加1。
因为某个原因,这个线程被剥夺(运行的机会)。
这时,对象所表示的数据模型是不一致的。
buffer|p|q|r||||
idx=2^
特别地,一致性会要求idx=3,或者还没有添加字符。
如果线程a恢复运行,那就可能不造成破坏,但假设线程b正等待移走一个字符。
在线程a等待另一个运行的机会时,线程b正在等待移走一个字符的机会。
pop()方法所指向的条目存在不一致的数据,然而pop方法要将下标值减1。
buffer|p|q|r||||
idx=1^
这实际上将忽略了字符“r”。
此后,它将返回字符“q”。
至此,从其行为来看,就好象没有推入字母“r”,所以很难说是否存在问题。
现在看一看如果线程a继续运行,会发生什么。
线程a从上次中断的地方开始运行,即在push()方法中,它将使下标值加1。
现在你可以看到:
buffer|p|q|r||||
idx=2^
注意这个配置隐含了:
“q”是有效的,而含有“r”的单元是下一个空单元。
也就是说,读取“q”时,它就象被两次推入了栈,而字母“r”则永远不会出现。
这是一个当多线程共享数据时会经常发生的问题的一个简单范例。
需要有机制来保证共享数据在任何线程使用它完成某一特定任务之前是一致的。
注-有一种方法可以保证线程a在执行完成关键部分的代码时不被调出。
这种方法常用在底层的机器语言编程中,但不适合多用户系统。
注-另外一种方法,它可以被Java技术采用。
这种方法提供精细地处理数据的机制。
这种方法允许无论线程是否会在执行存取的中间被调出,线程对数据的存取都是不可分割的,
13.7.2对象锁标志
对象锁标志
●每个对象都有一个标志,它可以被认为是“锁标志”。
●synchronized允许和锁标志交互。
在Java技术中,每个对象都有一个和它相关联的标志。
这个标志可以被认为是“锁标志”。
synchronized关键字使能和这个标志的交互,即允许独占地存取对象。
看一看下面修改过的代码片断:
publicvoidpush(charc){
synchronized(this){
data[idx]=c;
idx++;
}
}
当线程运行到synchronized语句,它检查作为参数传递的对象,并在继续执行之前试图从对象获得锁标志。
对象锁标志
意识到它自身并没有保护数据是很重要的。
因为如果同一个对象的pop()方法没有受到synchronized的影响,且pop()是由另一个线程调用的,那么仍然存在破坏data的一致性的危险。
如果要使锁有效,所有存取共享数据的方法必须在同一把锁上同步。
下图显示了如果pop()受到synchronized的影响,且另一个线程在原线程持有那个对象的锁时试图执行pop()方法时所发生的事情:
当线程试图执行synchronized(this)语句时,它试图从this对象获取锁标志。
由于得不到标志,所以线程不能继续运行。
然后,线程加入到与那个对象锁相关联的等待线程池中。
当标志返回给对象时,某个等待这个标志的线程将得到这把锁并继续运行。
13.7.3释放锁标志
释放锁标志
●线程执行到synchronized()代码块末尾时释放
●synchronized()代码块抛出中断或异常时自动释放
由于等待一个对象的锁标志的线程在得到标志之前不能恢复运行,所以让持有锁标志的线程在不再需要的时候返回标志是很重要的。
锁标志将自动返回给它的对象。
持有锁标志的线程执行到synchronized()代码块末尾时将释放锁。
Java技术特别注意了保证即使出现中断或异常而使得执行流跳出synchronized()代码块,锁也会自动返回。
此外,如果一个线程对同一个对象两次发出synchronized调用,则在跳出最外层的块时,标志会正确地释放,而最内层的将被忽略。
这些规则使得与其它系统中的等价功能相比,管理同步块的使用简单了很多。
13.7.4synchronized――放在一起
synchronized-放在一起
●所有对易碎数据的存取应当同步。
●由synchronized保护的易碎数据应当是private的。
正如所暗示的那样,只有当所有对易碎数据的存取位于同步块内,synchronized()才会发生作用。
所有由synchronized块保护的易碎数据应当标记为private。
考虑来自对象的易碎部分的数据的可存取性。
如果它们不被标记为private,则它们可以由位于类定义之外的代码存取。
这样,你必须确信其他程序员不会省略必需的保护。
一个方法,如果它全部属于与这个实例同步的块,它可以把synchronized关键字放到它的头部。
下面两段代码是等价的:
publicvoidpush(charc){
synchronized(this){
:
:
}
}
publicsynchronizedvoidpush(charc){
:
:
}
为什么使用另外一种技术?
如果你把synchronized作为一种修饰符,那么整个块就成为一个同步块。
这可能会导致不必要地持有锁标志很长时间,因而是低效的。
然而,以这种方式来标记方法可以使方法的用户由javadoc产生的文档了解到:
正在同步。
这对于设计时避免死锁(将在下一节讨论)是很重要的。
注意javadoc文档生成器将synchronized关键字传播到文档文件中,但它不能为在方法块内的synchronized(this)做到这点。
13.7.5死锁
死锁
●两个线程相互等待来自对方的锁
●它不能被监测到或避免
●它可以通过以下方法来避免
●决定获取锁的次序
●始终遵照这个次序
●按照相反的次序释放锁
如果程序中有多个线程竞争多个资源,就可能会产生死锁。
当一个线程等待由另一个线程持有的锁,而后者正在等待已被第一个线程持有的锁时,就会发生死锁。
在这种情况下,除非另一个已经执行到synchronized块的末尾,否则没有一个线程能继续执行。
由于没有一个线程能继续执行,所以没有一个线程能执行到块的末尾。
Java技术不监测也不试图避免这种情况。
因而保证不发生死锁就成了程序员的责任。
避免死锁的一个通用的经验法则是:
决定获取锁的次序并始终遵照这个次序。
按照与获取相反的次序释放锁。
第八节线程交互-wait
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第13章 线程 13