1、第5章 异常处理和多线程,5.1 异常与异常类 5.2 异常的处理 5.3 多线程,5.1 异常与异常类,异常(Exception)是程序执行期间发生的错误。在Java程序执行期间,产生的异常通常有三种类型:一是Java虚拟机由于某些内部错误产生的异常,这类异常不在用户程序的控制之内,也不需要用户处理这类异常;二是标准异常,这类异常通常是由程序代码中的错误产生的,例如被0除、数组下标越界等,这是需要用户程序处理的异常;三是根据需要在用户程序中自定义的一些异常。,返回,下一页,5.1 异常与异常类,5.1.1 异常处理机制 在Java语言中,所有的异常都是用类表示的。当程序发生异常时,会生成某个
2、异常类的对象。Throwable是java.lang包中一个专门用来处理异常的类,它有两个直接子类:Error和Exception。 Error类型的异常与Java虚拟机本身发生的错误有关,用户程序不需要处理这类异常。程序产生的错误由Exception的子类表示,用户程序应该处理这类异常。 Exception中定义了许多异常类,每个异常类代表了一种执行错误,类中包含了对应于这种运行错误的信息和处理错误的方法等内容。当程序执行期间发生一个可识别的执行错误时,如果该错误有一个异常类与之相对应,那么系统都会产生一个相应的该异常类的对象。一旦一个异常对象产生了,系统中就一定有相应的机制来处理它,从而保
3、证用户程序在整个执行期间不会产生死机、死循环等异常情况。Java语言采用这种异常处理机制来保证用户程序执行的安全性。,下一页,上一页,返回,5.1 异常与异常类,5.1.2 异常类的继承结构 Java语言的Exception类的继承结构如图5-1所示。 Exception类的每一个子类代表一种异常,这些异常表示程序执行期间各种常见的错误类型,它是Java系统事先定义好的并包含在Java语言类库中,称为系统定义的执行异常。表5-1对一些常见的系统定义的执行异常做了简要说明。 Exception类的两个构造函数是: public Exception(); public Exception(Stri
4、ng s); 其中,第二个构造函数可以接受字符串参数传入的信息,这个信息是对异常对象所对应的错误的描述。,下一页,上一页,返回,图5-1,返回,表5-1,返回,5.1 异常与异常类,Exception类也从父类Throwable那里继承了一些方法,最常用的两个方法是: public String toString(); public void printStackTrace(); 其中,第一个方法是返回描述当前Exception类信息的字符串。第二个方法的主要功能是在屏幕上输出异常信息,这个异常信息是由Java系统对用户程序执行过程中产生的异常方法进行跟踪时产生的,并由PrintStackTr
5、ace()方法输出到标准出错输出流,对于控制台程序来说,这个输出流就是屏幕。 Java程序在执行期间如果引发了一个Java系统能够识别的错误,就会产生一个与该错误相对应的异常类对象,这个过程称为抛出(throw)异常。所有Java系统定义的执行异常都可以由系统自动抛出。下面的例子用来测试在数组越界时出现的异常。,下一页,上一页,返回,5.1 异常与异常类,public class TestSystemException public static void main(String args) int num=new int3; for(int i=0;i4;i+) numi=i; System
6、.out.println(“ num“+i+“=“+i); 程序的运行结果如图5-2所示。,下一页,上一页,返回,图5-2,返回,5.1 异常与异常类,上例所示的程序在执行期间,由于在进行第四次for循环时存在着数组越界的错误,所以将引发ArrayIndexOutOfBoundsException异常。这个异常是Java系统已经定义好的一个异常类,所以Java系统遇到这个错误就自动终止程序的执行,并新建立一个ArrayIndexOutOfBoundsException类的对象,也就是抛出了一个数组越界异常。,上一页,返回,5.2 异常的处理,当程序在执行期间发生异常时,可以采取两种方法对异常进
7、行处理。一是由Java语言的异常处理机制来完成处理工作,但是采用这种处理方法时用户预先无法得知程序是发生了何种异常,用户无法对可能发生的异常做出适当的处理;二是用户使用Java系统提供的try-catch-finally组合语句处理可能的异常。这样一方面可以允许用户修正错误,另一方面可以避免因程序引起的异常而终止程序的执行。异常的处理通常由捕捉异常、程序流程的转移和异常处理语句块的定义3个执行步骤组成。,下一页,返回,5.2 异常的处理,5.2.1 捕捉异常 当程序抛出一个异常时,程序中应该有专门的语句来接收这个被抛出的异常对象,这个过程被称为捕捉(catch)异常。当一个异常类的对象被捕捉和
8、接收后,用户程序的执行流程就会发生转移,Java系统中止当前的程序流程而转到专门用来处理异常的语句块,或者直接终止当前程序的执行而返回到操作系统状态。 在进行程序设计时,为了避免因程序引起的异常而终止程序的执行,通常将监视异常的程序代码放在try语句块中。当程序代码出现异常时,这个try语句块就启动Java系统的异常处理机制来抛出一个异常对象,然后这个异常对象将被紧接在try语句块之后的catch语句块捕获。,下一页,上一页,返回,5.2 异常的处理,当异常对象被抛出后,程序的执行流程将按非正常、非线性的方式执行。如果此时在程序中没有匹配的catch语句块,那么程序将被终止而返回到操作系统状态
9、。为了避免发生这种情况,Java系统提供了finally语句块来解决这个问题。具体做法是,将finally语句块放在try与catch语句块之后,也就是说,不管异常对象被抛出还是没有被抛出,都将执行finally语句块。 try-catch-finally组合语句的一般语法格式如下: try / 可能发生异常的语句块 catch(异常类名 异常形式参数名) /处理异常的语句块 finally /无论是否发生异常都要被执行的语句块 ,下一页,上一页,返回,5.2 异常的处理,(1)catch语句括号内的异常类名指明了用户程序想要捕捉的异常类型。当程序执行期间发生异常时,catch语句块会捕捉这个
10、异常,并以语句块内的程序代码来处理异常。 (2)无论try语句块内的程序代码是否发生异常,finally语句块内的程序代码都会被执行。需要说明的是,finally语句块是可以省略的,而try语句块必须与catch或finally语句块之一配对使用。也就是说,try语句块单独使用是没有意义的。/TestCatchException.java public class TestCatchException public static void main(String args) int num=new int2;,下一页,上一页,返回,5.2 异常的处理,try for(int i=0;i3;i+
11、) numi=i; System.out.println(“ num“+i+“=“+i); catch(ArrayIndexOutOfBoundsException e)System.out.println(“数组下标越界引起的异常“); e.printStackTrace(); finally System.out.println(“程序执行期间发生了异常!“); 程序的运行结果如图5-3所示。,下一页,上一页,返回,图5-3,返回,5.2 异常的处理,5.2.2 异常的抛出 上小节介绍的是如何使用异常处理机制来捕捉系统已经定义好的异常类的对象,这些异常发生时会由Java系统自动地抛出。同时
12、,Java系统还允许用户在程序中自己定义异常类,并且用throw语句抛出,这就为Java程序的设计带来了更大的灵活性。 1. 用户自定义的异常类 用户自定义的异常类一般通过继承Exception类的形式来实现。下面是一个自定义异常类的程序段: public class MyException extends Exception /类体 ,下一页,上一页,返回,5.2 异常的处理,对于自定义的异常类,Java系统是不会自动为用户抛出属于该类的对象的,用户必须在程序中使用关键字throw来自行抛出异常对象。 使用关键字throw的一般语法格式如下: throw异常类对象; 这里的“异常类对象”既可
13、以是用户自定义的异常类,也可以是Java系统已经定义好的异常类。例如: TestThrowException.java class MyException extends Exception /自定义的异常类 String NewExceptionObject;,下一页,上一页,返回,5.2 异常的处理,public MyException() /构造函数 this.NewExceptionObject=“; public MyException(String s) /构造函数 this.NewExceptionObject=s; String ShowExceptionInfo() /返回接
14、收到的异常信息 return this.NewExceptionObject;,下一页,上一页,返回,5.2 异常的处理,public class TestThrowException public static void main(String args) try throw new MyException(“这是一个自行抛出的异常!“ ); /抛出异常 catch(MyException e) System.out.println(“MyException 类:“); System.out.println(“已经捕捉到抛出的异常对象!“); System.out.println(“异常信息
15、:“+e.ShowExceptionInfo() ); 程序的运行结果如图5-4所示。,下一页,上一页,返回,图5-4,返回,5.2 异常的处理,说明: (1)在程序中自定义了一个新的异常类MyException,该类自Exception类继承而得,并定义了两个构造函数和一个用来显示接收到的异常参数“这是一个自行抛出的异常!”的方法ShowExceptionInfo()。 (2)在主类TestThrowException的定义中,try语句块使用关键字throw将属于MyException类的对象抛出。由于抛出的异常对象类型与catch语句括号中的异常类MyException相匹配,所以cat
16、ch语句块将捕捉这个异常对象,并将调用MyException类中的方法ShowExceptionInfo(),将接收到的异常信息显示在屏幕上。 (3)该程序省略了finally语句块。,下一页,上一页,返回,5.2 异常的处理,2. 指定方法抛出的异常 如果一个方法不能处理它自己所引发的异常,那么异常处理的工作就需要由调用者来完成。在这种情况下,应该在方法的定义中使用关键字throws来指明该方法可能引发的所有异常,让调用者来处理这个异常。 包含throws子句的方法定义的一般语法格式如下: 修饰符 返回值类型 方法名(形式参数列表) throws 异常类 1,异常类 2,“ $“ /方法体
17、/TestThrowsException.java class MyException extends Exception String NewExceptionObject;,下一页,上一页,返回,5.2 异常的处理,public MyException() this.NewExceptionObject=“; public MyException(String s)this.NewExceptionObject=s; String ShowExceptionInfo() return this.NewExceptionObject; public class TestThrowsExcep
18、tion public static void main(String args) try Test(); ,下一页,上一页,返回,5.2 异常的处理,catch(MyException e) System.out.println(“MyException 类:“); System.out.println(“已经捕捉到抛出的异常对象!“); System.out.println(“异常信息:“+e.ShowExceptionInfo(); static void Test() throws MyException throw new MyException(“方法自身产生的异常将由调用者处理!
19、“ ); 程序的运行结果如图5-5所示。,下一页,上一页,返回,图5-5,返回,5.2 异常的处理,说明: (1)第112句中有关MyException类的定义与前面的例子完全一样。 (2)在main()方法中,第16句调用Test()方法,而Test()方法是通过throw语句(第25句)产生一个MyException类的异常,但是Test()方法本身并不能处理所引发的异常。 (3)由于抛出的异常对象类型与第18句的catch语句括号中的异常类MyException相匹配,所以catch语句块将捕捉这个异常对象,并将调用MyException类中的方法ShowExceptionInfo(),
20、将接收到的异常信息输出显示在屏幕上。,上一页,返回,5.3 多线程,5.3.1 线程与多线程 1. 线程 线程是存在于程序中的一个单独的顺序执行流程。 所有的编程人员都很熟悉编写顺序执行的程序,比如显示“Hello World!”、对数据进行简单排序、求方程的根等。这些程序有一个共同的特点:每个程序只有一个起始点、一个执行序列和一个结尾,在程序运行的某一特定时刻只有一个执行点。 对于一个线程来说,它很类似于一个顺序执行的程序,即一个单独的线程也只有一个起始点、一个执行序列和一个结尾,在线程运行的某一特定时刻也只有一个执行点。但是,一个线程只是一个程序的一部分,它本身并不能构成一个完整的程序,换
21、言之,程序可以独立运行,也可以拥有多个相互独立的线程,而线程则不然,它不能独立运行,也不能独立存在,而必须“寄生”于一个程序之中。,下一页,返回,5.3 多线程,只包含一个线程的程序就是我们所熟悉的顺序执行程序,这时线程这一概念并未给我们带来什么新意。而Java使用线程的神奇之处在于它使得一个程序可以使用多个线程,这些线程同时运行,而每个线程则完成不同的功能。 多线程的一个最典型的应用就是HotJava网络浏览器。在HotJava网络浏览器中,当正在下载一个小应用程序或一张图的时候,用户可以上下滚动页面,也可以使动画显示与声音同步等。引入多线程后,Java程序可以同时完成多项工作。 有些人称线
22、程为“轻量级的进程”。一个线程和一个进程都是一个单独的顺序执行流程,线程被认为是“轻量级”的原因在于线程位于一个完整的程序上下文之中,它利用了程序分配的资源和程序的运行环境。,下一页,上一页,返回,5.3 多线程,作为一个顺序执行流程,线程也必须拥有自己的运行资源。例如:它必须拥有自己的执行堆栈和程序计数器,线程的代码只能在该上下文中运行。因此,有人也把线程称为“执行上下文”。 2. 一个简单的多线程示例 下面是一个简单的线程示例。这个程序包含了SimpleThread和TwoThreadsTest两个类。其中,SimpleThread类是从java.1ang类组定义的类Thread派生而来的
23、。 class SimpleThread extends Thread public SimpleThread(String str) /构造过程, 设置线程的名称 super(str); /构造过程,通过引用父类的构造过程来实现 ,下一页,上一页,返回,5.3 多线程,public void run() /线程的核心过程 for(int i=0; i10; i+) System.out.println(i+“ “+getName(); try /Math.random()产生(0,1)上的均匀分布随机数 sleep(int)(Math.random()*1000); catch(Interr
24、uptedException e) System.out.println(“DONE!“+ getName(); ,下一页,上一页,返回,5.3 多线程,SimpleThread类的实现包含了两个过程。它的构造过程通过调用其父类的构造过程实现,设置了线程的名称。用户可以用Thread类的过程getName()获取这个名称。过程run()是SimpleThread类的核心过程。过程run()也是所有线程的核心过程,它定义线程的行为。SimpleThread类的过程run()中包含一个次数为10次的循环,每次循环输出循环变量值和线程的名称,并随机休眠一段(1 s以内)时间;当循环结束时,过程run
25、()显示“DONE!”信息和线程名,这就是线程SimpleThread的所有工作。 TwoThreadsTest类提供了main()过程,创建并启动了两个SimpleThread线程:“Threadl”和“Thread2”,其代码如下:,下一页,上一页,返回,5.3 多线程,class TwoThreadsTest public static void main(String args) new SimpleThread(“Thread1“).start(); /创建并启动线程 Thread1 new SimpleThread(“Thread2“).start(); /创建并启动线程 Thre
26、ad2 这个程序每次的输出结果是不一样的,如图5-6所显示的是某一次输出的结果。,下一页,上一页,返回,图5-6,返回,5.3 多线程,从上述输出结果不难看出:两个线程交错输出,这是因为两个SimpleThread线程是同时运行的,即它们的run()过程同时运行,因而一起显示各自的输出。 这里主要是向用户说明一个Java程序可以拥有多个线程,而这些线程是可以同时运行的。而该示例中所使用的sleep(), start()等过程将在后面讨论。,下一页,上一页,返回,5.3 多线程,5.3.2 创建线程 在Java语言中,可以用两种方法创建线程。本节中将分别进行介绍。 1. 创建线程的方法之一继承T
27、hread类 java.1ang.Thread是Java语言中用来表示进程的类,其中所定义的许多方法为完成线程的处理工作提供了比较完整的功能。如果将一个类定义为Thread的子类,那么这个类也就可以用来表示线程。 public class TwoThread public static void main(String args) ,下一页,上一页,返回,5.3 多线程,DelayPrintThread thread1,thread2; thread1 = new DelayPrintThread(); / 创建两个线程对象 thread2 = new DelayPrintThread();
28、thread1.start(); /开始执行两个线程 thread2.start(); try Thread.sleep( 10000 ); /主线程休眠10000 ms catch(InterruptedException e) System.out.println(“thread has wrong“); ,下一页,上一页,返回,5.3 多线程,class DelayPrintThread extends Thread private static int threadCount = 0; private int threadNumber = 0; private int delay; p
29、ublic DelayPrintThread() delay = (int)(Math.random()*5000);/计算休眠时间 threadCount+; /线程计数 threadNumber = threadCount; /线程号 public void run() try sleep( delay ); /子线程休眠一段时间 ,下一页,上一页,返回,5.3 多线程,catch ( InterruptedException e ) System.out.println( “This is Thread# “+threadNumber+“ with a delay of “+delay+
30、“.“ ); 运行结果如图5-7所示。 TwoThread类的main()方法中通过实例化两个DelayPrintThread不同的对象来创建两个执行线程。每个线程在它的start()方法被调用时开始执行。因为每个DelayPrintThread表示一个独立的线程,所以它们同时执行。程序的一次输出如屏幕图(图5-7)中所示。TwoThread程序的输出在每次执行时都将不一样,这是因为两个线程在Java执行系统中的执行顺序的不确定和这两个线程中的每一个都将睡眠一个随机产生的时间所造成的。,下一页,上一页,返回,图5-7,返回,5.3 多线程,2. 创建线程的方法之二实现Runnable接口 Ru
31、nnable是Java语言中用以实现线程的接口,从根本上讲,任何实现线程功能的类都必须实现该接口。前面所用到的Thread类实际上就是因为实现了Runnable接口,所以它的子类才相应具有线程功能。Runnable接口中只定义了一个方法,即run()方法,也就是线程体。 Thread的第二种构造方法中包含有一个Runnable实例的参数,这就是说,必须定义一个实现Runnable接口的类并产生一个该类的实例,对该实例的引用就是适合于这个构造方法的参数。,下一页,上一页,返回,5.3 多线程,class TwoThread implements Runnable /实现 Runnable 接口
32、TwoThread() /构造方法 Thread Thread1 = Thread.currentThread(); /定义线程 1 Thread1.setName(“The first main thread“); /设置线程名 System.out.println(“The running thread:“ + Thread1); Thread Thread2 = new Thread(this,“the second thread“); System.out.println(“creat another thread“); Thread2.start(); /启动线程 2 ,下一页,上一
33、页,返回,5.3 多线程,public void run() /重构 run 方法 try for ( int i = 0; i 5; i+ ) System.out.println(“Sleep time for thread :“+i); Thread.sleep(1000); /线程休眠 catch (InterruptedException e) System.out.println(“thread has wrong“); ,下一页,上一页,返回,5.3 多线程,public static void main(String args) new TwoThread(); /创建两个线程
34、对象 运行结果如图5-8所示。,下一页,上一页,返回,图5-8,返回,5.3 多线程,上例是通过继承接口的方式来创建、启动线程的。该程序执行结果为每隔1000 ms有一次输出显示,共循环5次。在main线程1中使用newThread(this,“the second thread“)创建另一个线程对象,通过传递第一个参数来标明新线程是调用this对象的run()方法,使线程2从run()方法开始执行,即当构造线程类的一个新的实例时,需要告诉它在新的线程里应该执行哪一段程序(run()方法)。 总之,线程由Thread对象的实例来引用。线程执行的代码来源于传递给Thread构造方法的参数引用的类
35、,这个类必须采用实现接口Runnable,线程操作的数据来源于传递给Thread构造方法的Runnable实例。,下一页,上一页,返回,5.3 多线程,3. 关于两种创建线程方法的讨论 既然两种方法创建线程效果相同,那么使用哪一种创建线程的方法更好?如何决定选择两种方法中的哪一种呢?下面分别列出了每种方法的适用范围。 (1)适用于采用实现Runnable接口的情况。由于Java语言只允许单继承,如果一个类已经继承了Thread,就不能再继承其他类,这时就被迫采用实现Runnable接口的方法。比如,对于Applet程序,由于必须继承java.applet.Applet,因此就只能采取这种实现接
36、口的方法。再有,由于上面的原因而几次被迫采用实现Runnable接口的方法,可能会出于保持程序风格的一贯性而继续使用这种方法。,下一页,上一页,返回,5.3 多线程,(2)适用于采用继承Thread方法的情况。当一个run()方法置于Thread类的子类中时,this实际上引用的是控制当前运行系统的Thread实例,所以,代码不必写得像下面这样繁琐: Thread.currentThread().suspend(); 而可简单地写为: suspend(); 因为代码稍微简洁一些,所以许多Java程序员愿意使用继承Thread的方法。但是应该知道,如果采取这种简单的继承模式,在以后的继承中可能会
37、出现麻烦。,下一页,上一页,返回,5.3 多线程,5.3.3 线程的启动 虽然一个线程已经被创建,但它实际上并没有立刻运行。要使线程真正在Java环境中运行,必须通过方法start()来启动,start()方法也在Thread类中。 例如,只要执行“Thread1.start();”,此时,线程中的虚拟CPU已经就绪,所以也可以把这一过程想象为打开虚拟CPU的开关。,下一页,上一页,返回,5.3 多线程,5.3.4 线程的调度 虽然就绪后线程已经可以运行,但它并不意味着这个线程一定能够立刻运行。显然,在一台实际上只具有一个CPU的机器上,CPU在同一时间只能分配给一个线程做一件事。那么现在就必
38、须考虑,当有多于一个的线程工作时,CPU是如何分配的。 在Java中,线程调度通常是抢占式,而不是时间片式。抢占式调度是指可能有多个线程准备运行,但只有一个在真正运行。一个线程获得执行权,这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,再或者有另一个高优先级线程就绪。最后一种情况称为低优先级线程被高优先级线程所抢占。,下一页,上一页,返回,5.3 多线程,一个线程被阻塞的原因是多种多样的,可能是因为执行了Thread.sleep()调用,故意让它暂停一段时间;也可能是因为需要等待一个较慢的外部设备,例如磁盘或用户。所有被阻塞的线程按次序排列,组成一个阻塞队列。而所有就绪但没有运行的
39、线程则根据其优先级排入一个就绪队列。当CPU空闲时,如果就绪队列不空,就绪队列中第一个具有最高优先级的线程将运行。当一个线程被抢占而停止运行时,它的运行状态被改变并放到就绪队列的队尾;同样,一个被阻塞(可能因为睡眠或等待I/O设备)的线程就绪后通常也放到就绪队列的队尾。 由于Java线程调度不是时间片式,所以在程序设计时要合理安排不同线程之间的运行顺序,以保证给其他线程留有执行的机会。为此,可以通过间隔地调用sleep()做到这一点。 例如:,下一页,上一页,返回,5.3 多线程,public class abc implements Runnable public void run() wh
40、ile(true) /执行若干操作 /给其他线程运行的机会 try Thread.sleep(20); catch(InterruptedException e) /该线程为其他线程所中断 ,下一页,上一页,返回,5.3 多线程,sleep()是Thread类中的静态方法,因此可以通过Thread.sleep(x)直接引用。参数x指定了线程在再次启动前必须休眠的最小时间,是以毫秒为单位的。同时该方法可能引发中断异常InterruptedException,因此要进行捕获和处理。这里说“最小时间”是因为这个方法只保证在一段时间后线程回到就绪状态,至于它是否能够获得CPU运行,则要视线程调度而定,
41、所以,通常线程实际被暂停的时间都比指定的时间要长。 除sleep()方法以外,Thread类中的另一个方法yield()可以给其他同等优先级线程一个运行的机会。如果在就绪队列中有其他同优先级的线程,yield()把调用者放入就绪队列尾,并允许其他线程运行;如果没有这样的线程,则yield()不做任何工作。,下一页,上一页,返回,5.3 多线程,5.3.5 结束线程 结束一个线程有两种情况:一种情况是当一个线程从run()方法的结尾处返回时它自动消亡并不能再被运行,可以将其理解为自然死亡;另一种情况是利用stop()方法强制停止,可以将其理解为强迫死亡,这种方法必须用于Thread类的特定实例中
42、。 例如: public class abc implements Runnable /执行线程的主要操作 public class ThreadTest public static void main(String args) Runnable r=new abc(); Thread t=new Thread(r); t.start(); /进行其他操作,下一页,上一页,返回,5.3 多线程,if (time_to_kill) t.stop(); 在程序代码中,可以利用Thread类中的静态方法currentThread()来引用正在运行的线程,例如: public class abc implements Runnable public void run() while (true) /执行线程的主要操作 if (time_to_die) Thread currentThread().stop(); ,上一页,返回,