第8章多线程与异常处理.docx

上传人:b****6 文档编号:5386463 上传时间:2022-12-15 格式:DOCX 页数:21 大小:852.50KB
下载 相关 举报
第8章多线程与异常处理.docx_第1页
第1页 / 共21页
第8章多线程与异常处理.docx_第2页
第2页 / 共21页
第8章多线程与异常处理.docx_第3页
第3页 / 共21页
第8章多线程与异常处理.docx_第4页
第4页 / 共21页
第8章多线程与异常处理.docx_第5页
第5页 / 共21页
点击查看更多>>
下载资源
资源描述

第8章多线程与异常处理.docx

《第8章多线程与异常处理.docx》由会员分享,可在线阅读,更多相关《第8章多线程与异常处理.docx(21页珍藏版)》请在冰豆网上搜索。

第8章多线程与异常处理.docx

第8章多线程与异常处理

第8章多线程与异常处理

8.1多线程的基本概念

8.1.1多任务

多任务

多任务是计算机操作系统同时运行几个程序或任务的能力。

现代操作系统都支持多任务,多任务有两种形式:

基于进程的多任务

基于线程的多任务

程序、进程和线程

程序是一段静态的代码,它是应用程序执行的蓝本。

进程是程序的一次动态执行过程,它对应了从代码加载、执行到执行完毕的一个完整过程。

程序可以被多次加载到系统的不同内存区域分别执行,形成不同的进程。

线程是进程内部的一个顺序执行控制流。

一个进程在执行过程中,可以产生多个线程同时执行。

每个线程也有自己产生、存在和消亡的过程。

8.1.2线程与多线程

线程和进程的区别:

从逻辑的观点来看,多线程意味着一个程序的多行语句同时执行,但是多线程并不等于多次启动一个程序,操作系统也不会把每个线程当作独立的进程来对待。

两者的层次不同,进程是由操作系统来管理的,而线程则是在一个程序(进程)内部存在的。

不同进程的代码、内部数据和状态都是完全独立的,进程之间进行切换和通信的开销很大。

线程本身的数据通常只有寄存器数据以及程序执行时使用的堆栈,一个程序内的多个线程是共享同一内存空间和系统资源,线程的切换开销小,线程之间的通信很容易。

8.1.3Java对多线程的支持

对多线程的综合支持是Java语言的一个重要特色,它提供了Thread类来实现多线程。

在Java中,线程可以认为是由三部分组成的:

虚拟CPU,封装在java.lang.Thread类中,它控制着整个线程的运行;

执行代码,传递给Thread类,由Thread类控制顺序执行;

处理的数据,传递给Thread类,是在代码执行过程中所要处理的数据。

8.1.4线程的状态

每个Java程序都有一个缺省的主线程,对于Application,主线程是执行main()方法的线程;对于Applet,主线程是通过浏览器加载并执行Java小程序的线程。

多线程应用的实质就是在主线程之外,定义了一个或多个新的线程。

Java使用Thread类及其子类表示线程。

一个线程的生命周期中,线程的状态表示了线程正在进行的活动以及在这段时间内线程能完成的任务。

新建状态(Newborn)

当创建了一个新的线程对象时,它就处于新建状态,此时它仅仅是一个空的线程对象,系统不为它分配资源。

处于这种状态时只能启动Start()或终止Stop()该线程,调用除这两种以外的其它方法都会失败并且会引起非法状态异常IllegalThreadStateException(对于其它状态,若所调用的方法与状态不符,都会引起非法状态异常)。

就绪状态(Runnable)

当线程处于新建状态时,可以调用start()方法来启动它,产生运行这个线程所需的系统资源,安排其运行,并调用线程体run()方法,这样该线程就处于就绪(Runnable)状态。

这一状态并不是运行状态(Running),因为线程也许实际上并未真正运行。

由于很多计算机都是单处理器的,所以要在同一时刻运行所有的处于就绪状态的线程是不可能的,Java运行系统必须通过调度来保证这些线程共享处理器。

阻塞状态(Blocked)

线程处于可运行状态时,当下面四种情况发生,线程就进入不可运行状态:

调用了休眠方法sleep()或yield()方法;

调用了挂起方法suspend();

为等候一个条件变量,线程调用等待方法wait();

输入输出流中发生线程阻塞。

ThreadmyThread=newMyThreadClass();

myThread.start();

try{

myThread.sleep(10000);

}catch(InterruptedExceptione){}

对于这四种使得线程处于不可运行状态的情况,都有特定的方法使线程返回可运行状态:

如果线程处于休眠状态中,sleep()方法中的参数为休眠时间,当这个时间过去后,线程即为可运行的;

如果线程在等待条件变量,那么要停止等待的话,需要该条件变量所在的对象调用notify()或notifyAll()方法;

如果在I/O流中发生线程阻塞,则特定的I/O指令将结束这种不可运行状态。

需要注意的是每种方法都仅仅对相应的状态才有作用,例如当一个线程休眠并且休眠时间还没有结束时,调用其他方法是无效的,并且还会引起非法状态异常。

死亡状态(Dead)

线程的终止一般可通过两种方法实现:

自然撤消或强行终止。

自然撤消是指从线程的run()方法正常结束退出,如果希望线程正常终止,一般可使用某种手段来使线程中的run()方法结束执行。

调用线程的实例方法stop()则可以强制停止当前线程。

既可以在其他线程中调用该线程的stop()方法来终止,也可以由线程自己调用stop()方法,自我终止。

但这个方法已在JDK2中建议不再使用,应当避免使用。

8.1.5线程的优先级

每个线程各自任务的重要程度不同而可能有不同的优先级。

Java提供一个线程调度器来监控程序中进入就绪状态的所有线程。

线程调度器按照线程的优先级决定调度线程的执行。

多个线程运行时,具有高优先级的线程会在较低优先级的线程之前得到执行。

线程的调度是抢先式的,一个具有更高优先级的线程进入可执行状态,则该高优先级的线程会被立即调度执行。

同一优先级的线程,采取先进先出的原则由操作系统按时间片轮转方式或独占方式来分配线程的执行时间。

线程的优先级用数字来表示,范围从1到10,Thread类有三个关于优先级的静态常量:

MIN_PRIORITY=1

MAX_PRIORITY=10

NORM_PRIORITY=5

对应于一个新线程,系统会遵循以下原则:

新线程将继承创建它的父线程的优先级。

一般情况下,线程具有普通优先级。

用户可以根据需要,通过setPriority()方法来修改优先级。

8.1.6线程的主要方法

Tread类综合了Java程序中一个线程所需要拥有的属性和方法,主要有:

publicThread(Runnabletarget,Stringname)

target是线程体run()方法所在的对象;

name是线程的名称。

target必须实现接口Runnable。

在接口Runnable中只定义了一个方法voidrun()作为线程体。

任何实现接口Runnable的对象都可以作为一个线程的目标对象。

start()方法:

启动线程,使线程由新建状态转为就绪状态;

run()方法:

定义该线程的操作,run()方法比较特殊,它可以被系统自动识别并执行。

sleep()方法:

使线程进入到休眠状态。

让其它线程得到机会执行。

sleep()会抛出异常InterruptedException,必须捕获。

isAlive()方法:

可以用来判断线程目前是否正在执行状态中。

如果线程已被启动并且未被终止,那么isAlive()返回true,但该线程是可运行或是不可运行的,需要作进一步的判断。

如果返回false,则该线程是新创建或是已被终止的(同样也需要作进一步的判断)。

join()方法:

等待另一个线程终止,即接着另一个线程执行。

yield()方法:

将执行的权力交给其它优先级相同的线程,自己到可运行线程队列的最后等待,若队列空,该方法无效。

8.2线程的使用方法

Java中编程实现多线程有两种方式:

由Thread类派生子类创建线程类;

通过实现Runnable接口创建线程类。

两种方法都要使用到Java.lang中的Thread类及其方法来实现。

当生成一个Thread对象之后,就产生了一个线程,通过该对象,可以启动线程、终止线程、或者暂时休眠等。

Thread类本身只是线程的虚拟CPU,线程所执行的代码(或者说线程所要完成的功能)是通过方法run()来完成的,run()方法称为线程体。

在一个线程被建立并初始化以后,Java的运行时系统就自动调用run()方法,完成线程要达到的目标。

8.2.1通过继承Thread类构造线程

线程的创建与启动

通过继承Thread类创建并执行一个线程,须完成下列步骤:

创建一个类扩展Thread类(extendsThread)。

用要在这个线程中执行的代码编写run()方法,覆盖Thread类的run()方法。

用关键字new创建所定义的线程类的一个对象。

调用该对象的start()方法启动线程。

由于Java只支持单继承,所定义的类不能再继承其他类。

【例8.1】创建一个Thread类的子类,显示1~8这8个数字,然后在另一个类中建立这个Thread类的3个对象来测试它,看执行时会发生什么现象。

终止线程执行的技巧

如下步骤实现一种简单的机制,可在任何时候终止线程:

编写一个类扩展Thread类。

增加一个布尔变量running到这个类中,并初始化为false。

覆盖start()方法:

置running为true,然后调用super.start()。

提供一个public方法halt(),它将running变量置为false。

在run()方法中使用类似于下例的while循环:

publicvoidrun(){

while(running){/*线程要执行的代码*/}

}

如果halt()方法被调用,就会引起run()方法终止执行,结束该线程。

【例8.2】模拟一个笼子内有二十个鸟在里面移动,每个“鸟”是一个Thread对象。

“笼子”是一个Frame。

它包含有3个按钮,用于启动、终止“鸟”和退出程序。

“鸟”类可按如下步骤去实现:

·扩展Thread,这样就是独立执行的线程,即鸟可独立行走。

·按上述终止线程的技巧,使得鸟类可在任何时刻停止执行。

·设置两个域x,y,作为鸟的当前坐标。

·编写move()方法,重新移动鸟到一个新的x,y坐标,在方法中重新计算它的坐标,使它产生移动效果。

·由于有多个移动的鸟,所以在每个线程的run()方法中应该调用sleep()方法,让出时间使操作系统去移动其他的鸟。

8.2.2通过实现Runnable接口来构造线程

Runnable接口

Runnable接口只有一个方法run(),所有实现Runnabel接口的用户类都必须实现这个run()方法,即为它编写具体的方法体代码。

在许多情况下,一个类已经扩展了Frame或Applet,由于单继承性,这样的类就不能再继承Thread。

Runnable接口为一个类提供了一种手段,无须扩展Thread类就可以创建一个新的线程。

从而克服了单一继承方式所造成的限制。

所有实现了Runnable接口的类的对象都能以线程方式执行。

使用Runnable接口构造线程要在一个类中实现publicvoidrun()方法,并建立一个Thread对象作为该类的成员变量。

实现Runnable接口的类的一般框架为:

[modifier]classClassName[extendsSuperClassName]

implementsRunnable[,OtherInterfaceList]

{//域,构造方法和其他方法

Threadthreadobj;

//建立一个Thread对象作为该类的成员变量

SomeMethod{threadobj=newThread(this);

//调用Thread的构造方法实例化一个对象

}

publicvoidrun()

{/*run方法的代码*/}

}

【例8.3】创建一个程序,用实现Runnable接口的方法来完成每秒显示当前时间。

在Java的java.util包中有一个Date类,实例化一个Date对象可得到当前时间,再用toString()方法可将其变成字符串。

下面要确定是否需要一个线程来完成这个任务。

要求每秒显示一次时间,线程是完成这个任务最好的角色。

可以每秒执行线程一次,且在这一瞬间显示出时间,其他时间可让线程休眠。

然后要考虑是需要扩展Thread类还是使用Runnable接口。

假如这个时钟要以窗口的形式出现,则这个线程必须选择使用Runnable接口来实现。

ShowSeconds.java

两种创建线程方式的比较

使用Runnable接口

可以将虚拟CPU,代码和数据分开,形成清晰的模型;

还可以类继承其他类;

保持程序风格的一致性。

继承Thread类

不能再从其他类继承;

编写简单,可以直接操纵线程。

无论采取上述的哪种方式,程序员可控制的关键操作有两个:

定义用户线程的操作,即定义用户线程的run()方法;

在适当的时候建立用户线程实例。

8.3线程的同步

线程间的资源互斥共享

通常,一些同时运行的线程需要共享数据。

在这种时候,每个线程就必须考虑其它一起共享数据的线程的状态与行为,否则的话就不能保证共享数据的一致性以及程序的正确性。

在Java语言中,引入了“对象互斥锁”的概念(又称为管程)来实现不同线程对共享数据操作的同步。

“对象互斥锁”阻止多个线程同时访问同一个方法。

在Java语言中,实现“对象互斥锁”的方式一般为:

用关键字synchronized来声明一个操作共享数据的方法或一段代码。

线程间的同步

除了要处理多线程间共享数据操作的同步问题之外,在进行多线程程序设计时,还会遇到另一类问题,这就是如何控制相互交互的线程之间的运行进度,即多线程的同步。

典型的模型:

生产者——消费者问题

若共享对象中只能存放一个数据,可能出现以下问题:

生产者比消费者快时,消费者会漏掉一些数据没有取到;

消费者比生产者快时,消费者取相同的数据。

8.3.1使用多线程不当造成的数据不一致

多线程使用不当,则有可能造成数据不一致。

例如,两个线程都要访问同一个整数域,一个线程读取这个域的值并在这个值的基础上完成某些操作,而另一个线程改变了这个整数值,但第一个线程并不知道,这就可能造成数据不一致。

【例8.4】创建一个类,它包含一个具有随机值的域number和一个performWork()方法,该方法完成两个任务:

一个“慢”任务和一个“快”任务。

完成这些任务并不是直接调用这个方法,而是建立两个线程对象:

一个快线程,一个慢线程。

·“慢”线程调用performwork并显示一个随机数,然后让这个线程休眠2秒钟,以模拟一个需要较长时间才能完成的任务,然后再次显示这个number。

·“快”线程在performwork中将number修改为-1,该线程没用延时,以模拟一个能够很快完成的任务。

8.3.1使用多线程不当造成的数据崩溃

【例8.5】建立一个银行(Bank)类,它包含8个储蓄账号。

每个账号最初有10万元余额。

Bank类包含一个转账的transfer方法,可以将钱从一个储蓄账号转到其他账号。

储户线程使用transfer()方法不断从他们的账号中转出随机数目(但不超过1000元)的钱到其他随机选定的账号。

transfer()方法需要3个输入参数:

从中取钱的账号,存钱的账号,转账的金额。

Bank类还有一个showAccounts()方法显示所有的账户的总金额。

Bank类创建8个线程对象模拟八个客户。

这个程序在一个Frame中执行,并设置按钮来重新进行计算和显示各账号的储蓄总额。

8.3.2同步线程

为了解决所出现的问题,Java提供了线程的同步机制用于保护线程共享数据,控制和切换线程的执行,保证内存的一致性,声明synchronized()方法的一般格式为:

synchronizedreturnTypemethodName([parameterList])

{/*方法体*/}

synchronized关键字除了可以放在方法声明中表示整个方法为同步方法外,还可以放在一段代码的前面限制它的执行,如:

[modifier]returnTypemethodName([parameterList])

synchronized(this){/*somecodes*/}

另外,如果synchronized用在类声明中,则表明该类中的所有方法都是synchronized的。

例8.6例8.7

在Java语言中还可以用wait()和notify()/notifyAll()方法用来协调线程间的读取关系。

wait()方法的作用是让当前线程释放其所持有的“对象互斥锁”,进入wait队列(等待队列);

notify()/notifyAll()方法的作用是唤醒一个或所有正在等待队列中等待的线程,并将它(们)移入等待同一个“对象互斥锁”的队列。

需要指出的是:

notify()/notifyAll()方法和wait()方法都只能在被声明为synchronized的方法或代码段中调用。

异常的概念

在进行程序设计时,错误的产生是不可避免的,比如溢出、数组越界、文件找不到等,发生这些事件将阻止程序的正常运行。

如何处理错误?

把错误交给谁去处理?

程序又该如何从错误中恢复?

这是任何程序设计语言都要解决的问题。

在C语言中,通过使用if语句来判断是否出现了错误,同时,通过被调用函数的返回值感知所产生的错误事件,并进行处理。

但是,这种错误处理机制会导致不少问题。

通常,用全局变量Errno来存储一个异常事件的类型,这容易导致误用,因为一个Errno的值有可能在被处理前被另外的错误覆盖掉。

此外,错误处理常常求助于goto语句。

异常的概念

Java通过面向对象的方法来处理程序错误。

在Java中,程序的任何错误或不正常的执行都被称为异常。

在一个方法的运行过程中,如果发生了异常,则这个方法通过Java虚拟机生成一个代表该异常的对象(包含了该异常的详细信息),并把它交给运行时系统,运行时系统寻找相应的代码来处理这一异常。

我们把生成异常对象并把它提交给运行时系统的过程称为抛出(throw)一个异常。

异常产生后,运行时系统从生成异常的方法开始进行回朔,直到找到包含相应异常处理的方法为止,这一个过程称为捕获(catch)一个异常。

对前面的C程序,采用Java的异常处理方式来编写,即为:

try{

openTheFile;

determineitssize;

allocatetoomuchmemory;

read-File;

closeTheFile;

}catch(fileopenFailed){dosomething;}

catch(sizeDetermineFailed){dosomething;}

catch(memoryAllocateFailed){dosomething;}

catch(readFailed){dosomething;}

catch(fileCloseFailed){dosomething;}

}

与传统的方法比较,异常的优点包括:

把错误处理代码从常规代码中分离出来

把错误传送给调用堆栈

按错误类型和错误差别分组

系统提供了对于一些无法预测的错误的捕获和处理

克服了传统方法的错误信息有限的问题

8.4.1Java的出错类型

Java中所有的异常都由对象来表示,所有的异常都直接或间接地继承自Throwable类。

在Java类库的每个包中都定义了异常类,这些异常类分成两大类:

Error类及Exception类,后者是Java程序中需要大量处理的。

除了Java类库所定义的异常类之外,用户也可以通过继承已有的异常类来定义自己的异常类,并在程序中使用(利用throw抛出异常,用catch捕捉异常),这样异常类称为自定义异常。

Error:

由Java虚拟机生成并抛出,包括动态链接失败、虚拟机错误等,Java程序不做处理。

Exception:

一般程序中可预知的问题,可分为运行时和非运行时异常。

RuntimeException:

Java虚拟机在运行时生成的异常,如被0除等系统错误、数组下标超范围等,其产生比较频繁,过多处理对程序可读性和运行效率影响太大。

因此由系统检测,用户可不做处理;必要时,用户可对其处理。

非运行时异常产生的异常可能会带来意想不到的结果,因此Java编译器要求程序必须捕获所有的非运行时异常。

用户自己定义的异常

8.4.2异常类的定义

常见的系统定义的异常

ArithmeticException

ArrayIndexOutOfBoundsException

ArrayStoreException

IOException

FileNotFoundException

NullPointerException

MalformatedURLException

NumberFormatException

OutOfMemoryException

对于RuntimeException,程序编译不要求捕获这些异常。

例8.9

用户自定义的异常

当我们在设计自己的类时,希望用户不要滥用所提供的方法。

而当程序出现不希望发生事件时,希望程序能够从这种不希望的状态中恢复,这时就需要用到异常。

在选择异常类型时,可以使用Java类库中定义的异常类,也可以自己定义异常类。

自定义异常类是不能由Java系统监测到的异常(如数组下标越界等),而是由用户人为定义的异常。

自定义异常必须继承Throwable或Exception类,建议用Exception类或它的子类。

一般不把自定义异常作为Error的子类,因为Error通常被用来表示系统内部的严重故障。

自定义异常同样要捕获,但必须由使用者抛出。

当自定义异常是从RuntimeException及其子类继承而来时,该自定义异常是运行时异常,程序可以不捕获和处理。

当自定义异常是从Throwable、Exception及其它非RuntimeException类的异常类继承而来时,该自定义异常是编译时异常,也即程序中必须捕获并处理它。

创建自定义异常时,一般需要完成如下工作:

定义一个新的类,扩展Exception类或其他某个已经存在的异常类。

为新的异常类定义属性和方法,或重载父类的属性和方法,一般扩展Throwable的类都覆盖它的两个构造方法:

一个默认的构造方法和一个包含显示信息的构造方法。

自定义异常的形式为:

classMyExceptionextendsException(或某个异常类)

{…

}

【例8.10】自定义一个完整的异常类。

publicclassOutOfRangeExceptionextendsException{

publicOutOfRangeException(){};

publicOutOfRangeException(Stings){

super(s);

}

}

例:

在定义银行帐户类时,若取钱数大于余额则作为异常处理(InsufficientFundsException)。

设计思路:

产生异常的条件是余额少于取额,因此是否抛出异常要先判断该条件。

确定产生异常的方法,应该在取钱方法(withdrawal)中产生异常InsufficientFundsException。

处理异常安排在调用withdrawal的时候,因此withdrawal方法要声明异常,由调用它的方法捕获并处理异常。

要定义自己的异常类。

publiccla

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 其它

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1