《Java语言程序设计实验指导电子教案第08章.ppt》由会员分享,可在线阅读,更多相关《Java语言程序设计实验指导电子教案第08章.ppt(24页珍藏版)》请在三一办公上搜索。
1、第8章 多线程编程,支持多线程编程是Java语言的又一大特色。多线程是相对于进程或单线程而言的,它具有并发性、执行效率高的特点。本章将对Java中的多线程编程作初步介绍。,8.1 多线程编程概述8.2 线程的创建8.3 线程的优先级8.4 线程同步8.5 线程间通信8.6 线程的控制,Return,8.1 多线程编程概述,本节介绍多线程编程的基础知识,包括多线程的基本概念、Java的线程模型(线程优先级、同步性、消息传递)等方面的内容。,8.1.1 什么是多线程8.1.2 Java线程模型,Return,8.1.1 什么是多线程,同其他大多数编程语言不同,Java内置支持多线程编程(multi
2、threaded programming)。多线程程序包含两条或两条以上并发运行的部分,把程序中每个这样的部分都叫作一个线程(thread)。每个线程都有独立的执行路径,因此多线程是多任务处理的一种特殊形式。读者可能知道多任务处理,它实际上被所有的现代操作系统所支持。然而,多任务处理有两种截然不同的类型:基于进程的和基于线程的。搞清楚两者的区别是很重要的。对大多数读者来说,基于进程的多任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。因此基于进程的多任务处理的特点是允许你的计算机同时运行两个或更多的程序。举例来说,基于进程的多任务处理使你在运用文本编辑器的时候可以同时运行J
3、ava编译器。在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。而在基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序可以同时执行两个或者多个任务的功能。例如,一个文本编辑器可以在打印的同时格式化文本。所以,多进程程序处理“大图片”,而多线程程序处理细节问题。,Return,多线程程序比多进程程序需要更少的管理费用。进程是重量级的任务,需要分配给它们独立的地址空间。进程间通信是昂贵和受限的。进程间的转换也是很需要花费的。另一方面,线程是轻量级的选手。它们共享相同的地址空间并且共同分享同一个进程。线程间通信是便宜的,线程间的转换也是低成本的
4、。当Java程序使用多进程任务处理环境时,多进程程序不受Java的控制,而多线程则受Java控制。多线程可帮助你编写出CPU最大利用率的高效程序,使得空闲时间保持最低。这对Java运行的交互式的网络互连环境是至关重要的,因为空闲时间是公共的。例如,网络的数据传输速率远低于计算机处理能力,而本地文件系统资源的读写速度也远低于CPU的处理能力。当然,用户输入也比计算机慢很多。在传统的单线程环境中,程序必须等待每一个这样的任务完成以后才能执行下一步尽管CPU有很多空闲时间。多线程使你能够获得并充分利用这些空闲时间。,8.1.2 Java线程模型,Java运行系统在很多方面依赖于线程,所有的类库设计都
5、考虑到多线程。实际上,Java使用线程来使整个环境异步。这有利于通过防止CPU循环的浪费来减少无效部分。为更好地理解多线程环境的优势,我们可以将它与它的对照物相比较。单线程系统的处理途径是使用一种叫作轮询的事件循环方法。在该模型中,单线程控制在一无限循环中运行,轮询一个事件序列来决定下一步做什么。一旦轮询装置返回信号表明已准备好读取网络文件,事件循环调度控制管理到适当的事件处理程序。直到事件处理程序返回,系统中没有其他事件发生。这就浪费了CPU时间。这导致了程序的一部分独占了系统,阻止了其他事件的执行。总的来说,单线程环境,当一个线程因为等待资源时阻塞(block,挂起执行),整个程序停止运行
6、。,Java多线程的优点就在于取消了主循环/轮询机制。一个线程可以暂停而不影响程序的其他部分。例如,当一个线程从网络读取数据或等待用户输入时产生的空闲时间可以被利用到其他地方。多线程允许活的循环在每一帧间隙中沉睡一秒而不暂停整个系统。在Java程序中出现线程阻塞,仅有一个线程暂停,其他线程继续运行。线程存在多种状态。线程可以正在运行(running),只要获得了CPU时间它就可以运行;运行的线程可以被挂起(suspend),并临时中断它的执行;一个挂起的线程可以被恢复(resume),允许它从停止的地方继续运行;一个线程可以在等待资源时被阻塞(block);在任何时候,线程可以被终止(term
7、inate),这将立即中断运行。一旦终止,线程不能被恢复。线程的各状态间关系见教材P190页图8-1所示。,下面简要介绍与Java线程相关的几个概念,Java给每个线程安排优先级以决定与其他线程比较时该如何对待该线程。线程优先级是详细说明线程间优先关系的整数。作为绝对值,优先级是毫无意义的;当只有一个线程时,优先级高的线程并不比优先级低的线程运行的快。相反,线程的优先级是用来决定何时从一个运行的线程切换到另一个。这叫“上下文转换”(context switch)。决定上下文转换发生的规则很简单:l线程可以自动放弃控制。在I/O未决定的情况下,睡眠或阻塞由明确的让步来完成。在这种假定下,所有其他
8、的线程被检测,准备运行的最高优先级线程被授予CPU。l线程可以被高优先级的线程抢占。在这种情况下,低优先级线程不主动放弃,处理器只是被先占无论它正在干什么处理器被高优先级的线程占据。基本上,一旦高优先级线程要运行,它就执行。这叫做有优先级的多任务处理。当两个相同优先级的线程竞争CPU周期时,情形有一点复杂。对于Windows这样的操作系统,等优先级的线程是在循环模式下自动划分时间的。对于其他一些非Windows操作系统而,如Solaris 2.x,等优先级线程相对于它们的对等体自动放弃。如果不这样,其他的线程就不会运行。,1线程优先级,2同步性,由于多线程在程序中引入了一个异步行为,故在需要的
9、时候必须有加强同步性的方法。举例来说,如果你希望两个线程相互通信并共享一个复杂的数据结构,例如链表序列,就需要某些方法来确保它们没有相互冲突。也就是说,你必须防止一个线程写入数据而另一个线程正在读取链表中的数据。为此,Java在进程间同步性的老模式基础上实行了另外的一种方法:管程(monitor)。管程是一种由C.A.R.Hoare首先定义的控制机制。你可以把管程想象成一个仅控制一个线程的小盒子。一旦线程进入管程,所有线程必须等待直到该线程退出了管程。用这种方法,管程可以用来防止共享的资源被多个线程操纵。很多多线程系统将管程作为程序必须明确的引用和操作的对象。但Java提供一个清晰的解决方案,
10、不提供“Monitor”类;相反,每个对象都拥有自己的隐式管程,当对象的同步方法被调用时管程自动载入。一旦一个线程包含在一个同步方法中,没有其他线程可以调用相同对象的同步方法。这就使你可以编写非常清晰和简洁的多线程代码,因为同步支持是语言内置的。,3消息传递,当把程序分成若干线程后,就要定义各线程之间的联系。用大多数其他语言规划时必须依赖于操作系统来确立线程间通信,这样当然要增加花费。然而,Java提供了多线程间谈话清洁的、低成本的途径通过调用所有对象都有的预先确定的方法。Java的消息传递系统允许一个线程进入一个对象的一个同步方法,然后在那里等待,一直等到其他线程明确通知它出来。,Retur
11、n,Java的多线程系统建立于Thread类、方法以及共伴接口Runnable基础上。Thread类封装了线程的执行。既然不能直接引用运行着的线程的状态,就要通过它的代理处理它。于是Thread 实例产生了。为创建一个新的线程,程序中必须扩展Thread 或实现Runnable接口。Thread类定义了好几种方法来帮助管理线程,见教材P192页表8-1中所列。,4Thread类和Runnable接口,8.2 线程的创建,本节介绍在Java中如何创建线程。主要内容包括主线程、多线程的创建、相关方法的使用等。,8.2.1 关于主线程8.2.2 创建一个线程8.2.3 创建多线程8.2.4 使用is
12、Alive()和join(),Return,8.2.1 关于主线程,当Java程序启动时,一个线程立刻运行,该线程通常就叫做程序的主线程(main thread),因为它是程序开始时就执行的。主线程的重要性主要体现在两方面:l它是产生其他子线程的线程;l通常它必须最后完成执行,因为它执行各种关闭动作。尽管主线程在程序启动时自动创建,但它可以由一个Thread对象控制。为此,必须调用方法currentThread()获得它的一个引用,currentThread()是Thread类的公有的静态成员。它的一般形式如下 static Thread currentThread()该方法返回一个调用它的线
13、程的引用。一旦获得主线程的引用,就可以像控制其他线程那样控制主线程。下面我们考察一下教材P192193页的程序代码。在上面的程序中,当前线程(当然是主线程)的引用通过调用currentThread()获得,该引用保存在局部变量t中。然后,程序显示了线程的信息。接着,程序调用setName()改变线程的内部名称,线程信息又被显示。然后,一个循环数从5开始递减,每数一次暂停一秒。暂停是由sleep()方法来完成的,sleep()语句明确规定延迟时间是1毫秒。请读者注意循环外的try/catch块。Thread类的sleep()方法可能引发一个InterruptedException异常,这种情形会
14、在其他线程想要打搅沉睡线程时发生。本例只是打印了它是否被打断的消息。在实际的程序中,必须灵活处理此类问题。,Return,8.2.2 创建一个线程,大多数情况,通过实例化一个Thread对象来创建一个线程。Java定义了两种方式:l实现Runnable 接口;l以继承Thread类的方式。,创建线程最简单的方法就是创建一个实现Runnable 接口的类,Runnable抽象了一个执行代码单元。可以通过实现Runnable接口的方法创建每一个对象的线程。为实现 Runnable 接口,一个类仅需实现一个run()的简单方法,该方法声明如下:public void run()在run()中,可以定
15、义代码来构建新的线程。重要的是:run()方法能够像主线程那样调用其他方法,引用其他类,声明变量。仅有的不同是:run()在程序中确立另一个并发的线程执行入口。当run()返回时,该线程结束。在已经创建了实现Runnable接口的类以后,需要在类内部实例化一个Thread类的对象。Thread 类定义了好几种构造函数。我们会用到的如下:Thread(Runnable threadOb,String threadName)在该构造函数中,threadOb是一个实现Runnable接口类的实例。这定义了线程执行的起点,新线程的名称由threadName定义。建立新的线程后,它并不运行直到调用其st
16、art()方法,该方法在Thread 类中定义。从本质上讲,start()执行的是一个对run()的调用。start()方法声明如下:void start(),下面我们分别对这两种方法进行介绍:,1实现Runnable接口,2扩展Thread,创建线程的另一个途径是创建一个新类来扩展Thread类,然后再创建该类的实例。当一个类继承Thread时,它必须重载run()方法,这个run()方法是新线程的入口。同时,它也必须调用start()方法去启动新线程执行。,Return,到这里,读者可能会奇怪为什么Java有两种创建子线程的方法,哪一种更好呢。所有的问题都归于一点。Thread类定义了多种
17、方法可以被派生类重载。对于所有的方法,唯一的必须被重载的是run()方法。这当然是实现Runnable接口所需的同样的方法。很多Java程序员认为类仅在它们被加强或修改时被扩展。因此,如果你不重载Thread的其他方法,最好只实现Runnable 接口,这当然由自己决定。在本章的其他部分,我们应用实现Runnable接口的类来创建线程。,3选择合适的方法,8.2.3 创建多线程,到目前为止,我们仅用到两个线程:主线程和一个子线程。然而,我们完全可以创建所需的更多线程。例如,教材P197198页的程序创建了3个子线程。详细分析该程序。,Return,8.2.4 使用isAlive()和join(
18、),如前所述,我们一般是希望主线程最后结束。在上面的例子中,这点是通过在main()中调用sleep()来实现的,经过足够长时间的延迟以确保所有子线程都先于主线程结束。然而,这并不是一个好的解决方法。因为有时候存在这个问题:一个线程如何知道另一线程已经结束?幸运的是,Thread类提供了解决此问题的有效方法。有两种方法可以判定一个线程是否结束:第一,可以在线程中调用isAlive()。这种方法由Thread定义,它的一般形式如下final boolean isAlive()如果所调用线程仍在运行,isAlive()方法返回true,如果不是则返回false。但isAlive()很少用到,等待线
19、程结束的更常用的方法是调用join(),描述如下final void join()throws InterruptedException 该方法等待所调用线程结束,该名字来自于要求线程等待直到指定线程参与的概念。join()的附加形式允许给等待指定线程结束定义一个最大时间。详细分析教材P199200页的程序。,Return,8.3 线程的优先级,线程优先级被线程调度用来判定何时某个线程允许运行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在内的多个因素决定。一个优先级高的线程自然比优先级低的线程优先。理论上,等优先级线程有同等的权利使
20、用CPU,但你必须小心。需要记住的是,Java是被设计成能在很多环境下工作的。不同环境下实现多任务处理从本质上来看是可能的。为安全起见,等优先级线程有时候也受到控制。这保证了所有线程在无优先级的操作系统下都有机会运行。实际上,在无优先级的环境下,多数线程仍然有机会运行,因为很多线程不可避免地会遭遇阻塞,例如等待输入输出。遇到这种情形,阻塞的线程挂起,其他线程运行。但是如果你希望多线程执行得顺利的话,最好不要采用这种方法。同样,有些类型的任务是占CPU的。对于这些支配CPU类型的线程,有时你希望能够支配它们,以便使其他线程可以运行。设置线程的优先级,用setPriority()方法,该方法也是T
21、hread的成员。它的通常形式为 final void setPriority(int level)这里,level指定了对所调用的线程的新的优先权的设置。Level的值必须在MIN_PRIORITY到MAX_PRIORITY范围内。通常,它们的值分别是1和10。要返回一个线程为默认的优先级,指定NORM_PRIORITY,通常值为5。这些优先级在Thread中都被定义为final型变量。,Return,8.4 线程同步,当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用,达到此目的的过程叫做同步(synchronization)。Java为此提供了独特的、
22、很有效的支持机制。,8.4.1 使用同步方法8.4.2 同步语句,Return,8.4.1 使用同步方法,同步的关键是管程(也叫信号量,即semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程可以获得管程。当一个线程需要锁定时,它必须进入管程。所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其他的线程被称为等待管程。我们可以用两种方法同步化代码。通过调用sleep(),call()方法允许执行转换到另一个线程。两者都包括synchronized关键字的运用。分析教材P204页的示例。,Return,8.4.2 同
23、步语句,尽管在创建的类的内部创建同步方法是获得同步的简单和有效的方法,但它并非在任何时候都有效。假设你想获得不为多线程访问设计的类对象的同步访问,也就是该类没有用到synchronized方法。而且,该类不是你自己,而是第三方创建的,就不能获得它的源代码。这样,就不能在相关方法前加synchronized修饰符。怎样才能使该类的一个对象同步化呢?解决的方法很简单:只需将对这个类定义的方法的调用放入一个synchronized块内就可以了。下面是synchronized语句的一般形式 synchronized(object)/statements to be synchronized 其中,ob
24、ject是对同步对象的引用。如果你想要同步的只是一个语句,那么不需要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。,Return,8.5 线程间通信,前面的例子无条件地阻塞了其他线程异步访问某个方法。Java对象中隐式管程的应用是很强大的,但是我们可以通过进程间通信达到更微妙的境界,这在Java中是很简单的。,8.5.1 Java中的线程通讯8.5.2 关于死锁,Return,8.5.1 Java中的线程通讯,多线程通过把任务分成离散的和合乎逻辑的单元代替了事件循环程序。线程还有另外一个优点:它远离了轮询。轮询通常由重复监测条件的循环实现。一旦条
25、件成立,就要采取适当的行动。这浪费了CPU时间。为避免轮询,Java包含了通过wait(),notify()和notifyAll()方法实现的一个进程间通信机制。这些方法在对象中是用final方法实现的,所以所有的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来说具有概念的高度先进性,实际中用起来却是很简单的。wait()告知被调用的线程放弃管程进入睡眠直到其他线程进入相同管程并且调用notify()。notify()恢复相同对象中第一个调用wait()的线程。notifyAll()恢复相同对象中所有调用wait()的线程。这些方法在O
26、bject中被声明,如下所示final void wait()throws InterruptedExceptionfinal void notify()final void notifyAll()wait()存在的另外的形式允许你定义等待时间。分析教材P207210页的程序段。,Return,8.5.2 关于死锁,需要避免的与多任务处理有关的特殊错误类型是死锁(deadlock)。死锁发生在当两个线程对一对同步对象有循环依赖关系时。例如,假定一个线程进入了对象X的管程而另一个线程进入了对象Y的管程。如果X的线程试图调用Y的同步方法,它将像预料的一样被锁定。而Y的线程同样希望调用X的一些同步方
27、法,线程永远等待,因为为到达X,必须释放自己的Y的锁定以使第一个线程可以完成。死锁是很难调试的错误,这是因为:第一,通常它极少发生,只有到两线程的时间段刚好符合时才能发生;第二,它可能包含多于两个的线程和同步对象。也就是说,死锁在比刚讲述的例子有更多复杂的事件序列的时候可以发生。为充分理解死锁,观察它的行为是很有用的。教材P211212页的例子生成了两个类,A和B,分别有foo()和bar()方法。这两种方法在调用其他类的方法前有一个短暂的停顿。主类,名为Deadlock,创建了A和B的实例,然后启动第二个线程去设置死锁环境。foo()和bar()方法使用sleep()强迫死锁现象发生。程序死
28、锁,需要按CTRL-C来结束程序。在PC机上按CTRL-BREAK(或在Solaris下按CTRL-)可以看到全线程和管程缓冲堆。,Return,8.6 线程的控制,本节讨论有关线程控制的问题,包括线程的挂起、恢复、终止等方面的问题。,8.6.1 挂起、恢复和终止线程8.6.2 Java 2中的线程控制8.6.3 使用instanceof,Return,8.6.1 挂起、恢复和终止线程,有时,线程的挂起是很有用的。例如,一个独立的线程可以用来显示当日的时间。如果用户不希望用时钟,线程被挂起。在任何情形下,挂起线程是很简单的,一旦挂起,重新启动线程也是一件简单的事。挂起、终止和恢复线程机制在Ja
29、va 2和早期版本中有所不同。尽管你运用Java 2的途径编写代码,仍需了解这些操作在早期Java环境下是如何完成的。例如,也许你需要更新或维护老的代码,就需要了解为什么Java 2会有这样的变化。因为这些原因,下面内容说明了执行线程控制的原始方法,接着是Java 2的方法。先于Java2的版本(Java 1.1或更早版本),程序用Thread 定义的suspend()和 resume()来暂停和再启动线程。它们的形式如下。final void suspend()final void resume()Thread类同样定义了stop()来终止线程,其形式如下:void stop()一旦线程被终
30、止,它不能被resume()恢复继续运行。,Return,8.6.2 Java 2中的线程控制,在Java 2中不能使用suspend(),resume()和stop()方法来控制线程,读者也许会想那就没有办法来停止、恢复和结束线程,其实不然。相反,线程必须被设计成使用run()方法定期检查来判定线程是否应该被挂起,恢复或终止它自己的执行。有代表性的,这由建立一个指示线程状态的标志变量来完成。只要该标志设为“running”,run()方法必须继续让线程执行。如果标志设为“suspend”,线程必须暂停。若设为“stop”,线程必须终止。当然,编写这样的代码有很多方法,但中心主题对所有的程序应
31、该是相同的。教材P216217页的例子显示了从Object继承的wait()和notify()方法怎样控制线程的执行。该例与前面讲过的程序很像。然而,不被赞同的方法都没有用到。让我们思考程序的执行。NewTread类包含了用来控制线程执行的布尔型的实例变量suspendFlag。它被构造函数初始化为false。Run()方法包含一个监测suspendFlag的同步声明的块。如果变量是true,wait()方法被调用以挂起线程。Mysuspend()方法设置suspendFlag为true。Myresume()方法设置suspendFlag为false并且调用notify()方法来唤起线程。最后
32、,main()方法被修改以调用mysuspend()和myresume()方法。,Return,8.6.3 使用instanceof,最后,我们附带介绍一下关键字instanceof。有时,在运行时间内知道对象类型是很有用的。例如,你有一个执行线程生成各种类型的对象,其他线程处理这些对象。这种情况下,让处理线程在接受对象时知道每一个对象的类型是大有裨益的。另一种在运行时间内知道对象的类型是很有用的情形是强制类型转换。Java中非法强制类型转换导致运行时错误。很多非法的强制类型转换在编译时发生。然而包括类层次结构的强制类型转换可能是仅能在运行时间里被察觉的非法强制类型转换。例如,一个名为A的父类
33、能生成两个子类B和C。这样,在强制B对象转换为类型A或强制C对象转换为类型A都是合法的,但强制B对象转换为C对象(或相反)都是不合法的。因为类型A的一个对象可以引用B或C。但是你怎么知道,在运行时,在强制转换为C之前哪类对象被引用?它可能是A、B或C的一个对象。如果它是B的对象,一个运行时异常被引发。Java 提供运行时运算符instanceof来解决这个问题。instanceof运算符具有下面的一般形式:object instanceof type 这里,object是类的实例,而type是类的类型。如果object是指定的类型或者可以被强制转换成指定类型,instanceof将它评估成true,若不是,则结果为false。这样,instanceof是程序获得对象运行时类型信息的方法。教材P218219页的程序说明了instanceof的应用。,Return,