JAVA线程与异常处理

Java有两个机制:多线程(Multithread)和异常处理(Exception)。本章前半部分是关于Thread这一基本类以及一套先进的同步原语的介绍,它们使得利用Java编写多线程大为方便。在本章的后半部分我们将介绍Java的异常处理机制(Exception),异常处理机制提高了程序的健壮性。另外,本章中间将介绍一个Java的debugger工具Jdb的使用,Jdb工具对于调试多线程程序尤其有好处。

 5.1 多线程(Multithread)

  5.1.1 线程的基本概念

在介绍多线程之前,我们先来了解一些相关的基本概念。一般来说,我们把程序的一次执行称为进程(process)。一个进程包括一个程序模块和该模块一次执行时所处理的数据。每个进程与其它进程拥有不同的数据块,其内存地址是分开的。进程之间的通信要通过寻址,一般需使用信号、管道等进行通信。线程(thread)是指进程内部一段可独立执行的有独立控制流的指令序列。子线程与其父线程共享一个地址空间,同一个任务中的不同线程共享任务的各项资源。
多进程与多线程是多任务的两种类型。以前的操作系统,如Win31,只运行多进程,而Win95及WinNT则支持多线程与多进程。Java通过提供Package类(Java.lang.package)支持多进程,而提供Thread类来支持多线程。
多线程与多进程的主要区别在于,线程是一个进程中一段独立的控制流,一个进程可以拥有若干个线程。在多进程设计中各个进程之间的数据块是相互独立的,一般彼此不影响,要通过信号、管道等进行交流。而在多线程设计中,各个线程不一定独立,同一任务中的各个线程共享程序段、数据段等资源,如图5.1。
正如字面上所表述的那样,多线程就是同时有多个线程在执行。在多CPU的计算机中,多线程的实现是真正的物理上的同时执行。而对于单CPU的计算机而言,实现的只是逻辑上的同时执行。在每个时刻,真正执行的只有一个线程,由操作系统进行线程管理调度,但由于CPU 的速度很快,让人感到像是多个线程在同时执行。
多线程比多进程更方便于共享资源,而Java又提供了一套先进的同步原语解决线程之间的同步问题,使得多线程设计更易发挥作用。用Java设计动画以及设计多媒体应用实例时会广泛地使用到多线程,在后面几章你将看到多线程的巨大作用,当然,现在必须先学习一些多线程的基本知识,慢慢地你就体会到它的优越性。

5.1.2 线程的状态

如同进程有等待、运行、就绪等状态一样,线程也有其状态。
当一个线程通过new被创建但还未运行时,称此线程处于准备状态(new状态)。当线程调用了start()方法或执行run()方法后,则线程处于可运行状态。若在等待与其它线程共享资源,则称线程处于等待状态。线程的另一个状态称为不可运行(not runnable)状态,此时线程不仅等分享处理器资源,而且在等待某个能使它返回可运行状态的事件,例如被方法suspend()挂起的进程就要等待方法resume()方可被唤醒。当调用了stop()方法或线程执行完毕,则线程进入死亡(dead)状态。线程的各个状态之间的转换关系见图5.2。

5.1.3 创建线程

在了解基本概念后,下面学习如何在Java中创建多线程。
Java通过java.lang.Thread类来支持多线程。在Thread类中封装了独立的有关线程执行的数据和方法,并将多线程与面向对象的结构合为一体。
Java提供了两种方法创建线程,一种是继承Thread类,另一种则是实现接口Runnable。
1.继承Thread类
通过继承Thread类创建线程十分简单,只需要重载run()方法提供执行入口就可以,下面我们通过例5.1来解释说明。
例5.1 ThreadTest1.java。

  1. import java.lang.Thread;
  2. import java.lang.System;
  3. import java.lang.Math;
  4. import java.lang.InterruptedException;
  5. class ThreadTest1{
  6. public static void main(String args[])
  7.   throws java.io.IOException{
  8.   System.out.println(“If want to show the result,press return”);
  9.   MyThread thread1=new MyThread(“thread1”);
  10.   MyThread thread1=new MyThread(“thread2”);//创建了两个线程thread1和thread2
  11.   thread1.start();//开始执行线程
  12.   thread2.start();
  13.   char ch;
  14.   while((ch=(char)System.in.read()) != ‘ ‘);//不断循环,等待输入回车符
  15.   thread1.tStart();//改变thread1和thread2中的循环控制变量的值
  16.   thread2.tStart();//以下部分保证main()方法是最后一个结束的
  17.   while((thread1.isAlive())|(thread2.isAlive()));
  18.   /*{
  19.   you can do anything that you want to do here.
  20.   }
  21.   */
  22.   System.out.println(“The test is end.”);
  23. }
  24. }
  25. //类MyThread继承了类Thread
  26. class MyThread extends Thread{
  27. private boolean keepRunning=true;
  28. public MyThread(String id){//类MyThread的构造方法
  29.   super(id);
  30. }
  31. void randomWait(){//让线程处于等待状态
  32.   try{
  33.     sleep((long)(3000*Math.random()));
  34.   }
  35.   catch(InterruptedException x){
  36.     System.out.println(“Interrupted!”);
  37.   }
  38. }
  39. public void tStart(){
  40.   keepRunning=false;
  41. }
  42. public void run(){//重写了类Thread中的方法run(),main()中调用Thread的方法start()后将自动调用此方法
  43. int i=0;
  44. while(keepRunning) i++;//i代表循环次数
  45. //输出结果
  46. for(int j=0;j<=3;i++){
  47.   randomWait();
  48.   System.out.println(“I am”+getName()+”—— I have run”+i+”times.”);
  49.   i++;
  50.   }
  51.   System.out.println(getName()+” is dead!”);
  52. }
  53. }

这个程序中创建了两个线程thread1和trhrad2。每个线程将打印一些内容。当线程死亡时,将打印出线程死亡信息。试着执行一下这个程序,你将发现每次的结果都不尽相同。
运行结果:(略)
下面我们分析一下这个程序。为创建Thread,第一行你必须写import java.lang.Thread。行6~25中书写的类ThreadTest1包含了一个main()方法(行7~24)。行8的throws java.io.IOException暗示了main()方法中可以产生IOException(有关异常处理后面几节将详细介绍),这主要是为了调用方法System.in.read()实现输入功能。main()方法中创建了两个MyThread的对象,即行10的thread1与行11的thread2。
行28~55中书写的类MyThread是Thread类的子类。在类MyThread中重写了方法run()(行43~54)。在此种构造线程的方法中,这是必须的。行29~33定义了MyThread类的构造方法MyThread(String id)。行32和行40定义了方法randomWait()和tStart()。
在ThreadTest1的main()方法中,当行13调用thread1.start()与thread2.start()后,线程thread1与thread2进入可运行状态,分别自动执行其run()方法,而main()方法也继续执行,此时相当于有三个线程在同时执行。
看一下程序,此时thread1与thread2在执行第44~54行的run()方法,为断循环并累计循环次数,而main()则在等待循环直至入为回车符。 输入回车符,main()方法结束循环,继续执行,调用了MyThread中方法tStart()(41~43行),这样结束了Thread1与Thread2在run()方法中的循环,开始执行输出。由于thread1,thread2与main()三个线程轮流占有CPU,所以显示了各自结果,这也是为何多次执行结果不同的原因,此时体现了多线程的功能。
请注意一下Run()中调用了MyThread类中自定义的方法randomWait()(33~40行)。在randomWait()中调用了方法sleep(),sleep()方法是Thread类中的方法,它让正在执行的线程小睡片刻,进入不可运行状态(not runnable状态),当时间到时,线程会回复到runnable状态。当线程执行sleep()时,有可能会被打断,因而程序中加了一段处理InterruptedException的中断处理和打印信息,这样加强了程序的健壮性。
另外,人们会注意到,在main()中有一段循环并未完成什么功能,这是为了简化程序,其实那段时间中你可以做你想完成的任何工作。但请注意,在执行时,最后一个结束的必须是main()方法,而不可以是其它,否则执行结束将不返回C:提示符,这也是在程序中为何调用了isAlive()方法进行判别的原因。调用类Thread的isAlive()方法可以测试线程是否仍在运行状态(此外指还未死亡)。
至此,你已真正了解了你的第一个关于Thread的程序,其实Thread中还有很多方法在此未被使用,后面将会进一步介绍。下面先介绍一下创建Thtead的另一个方法:利用实现接口Runnable创建Thread。
2.实现接口Runnable
使用用类java.lang.Runnable中的接口Runnable也可创建线程。下面的例子与例5.1实现的功能相同,只是它利用接口Runnable来实现。
例5.2 ThreadTest2.java。

  1. import java.lang.Thread;
  2. import java.lang.System;
  3. import java.lang.Math;
  4. import java.lang.InterruptedException;
  5. import java.lang.Runnable;
  6. class ThreadTest2{
  7. public static void main(String args[])
  8.   throws java.io.IOException{
  9.   System.out.println(“If want to show the result,press return”);//创建了两个MyClass类的对象//class1和class2,MyClass类实现了接口Runnable
  10.   MyClass class1 = new MyClass(“thread1”);
  11.   MyClass class2 = new MyClass(“thread2”);//创建了两个MyClass类的对象class1和class2,MyClass类实现了接口Runnable
  12.   Thread thread1=new Thread(class1);
  13.   Thread thread2=new Thread(class2);//将对象class1和class2作为参数传给Thread类的构造函数,创建了两个线程thread1和thread2。
  14.   thread1.start();//开始执行线程
  15.   thread2.start();
  16.   char ch;
  17.   while((ch=(char)System.in.read()) != ‘ ‘);//不断循环,等待输入回车符
  18.   class1.tStart();//改变thread1和thread2中的循环控制变量的值
  19.   class2.tStart();//以下部分保证main()方法是最后一个结束的
  20.   while((thread1.isAlive())||(thread2.isAlive()));
  21.   /*{
  22.   you can do anything that you want to do here.
  23.   }
  24.   */
  25.   System.out.println(“The test is end.”);
  26. }
  27. }
  28. //类MyClass实现了接口Runnable
  29. class MyClass implements Runnable{
  30. boolean keepRunning=true;
  31. String name;
  32. public MyClass(String id){//类MyClass的构造方法
  33.   name=id;
  34. }
  35. void randomWait(){//让线程处于等待状态
  36.   try{
  37.     Thread.currentThread().sleep((long)(3000*Math.random()));//注意:接口Runnable中没有方法sleep(),所以必须先调用Thread的类方法currentThread()来获取一个Thread的对象,然后再调用方法seleep()
  38.   }
  39.   catch(InterruptedException x){
  40.     System.out.println(“Interrupted!”);
  41.   }
  42. }
  43. public void tStart(){
  44.   keepRunning=false;
  45. }
  46. public void run(){//与程序ThreadTest1.java类似
  47. int i=0;
  48. while(keepRunning) i++;//i代表循环次数
  49. //输出结果
  50. for(int j=0;j<=3;j++){
  51.   randomWait();
  52.   System.out.println(“I am “+name+”—— I have run “+i+” times.”);
  53.   i++;
  54.   }
  55.   System.out.println(name+” is dead!”);
  56. }
  57. }

 

运行结果:(略)
ThreadTest2创建thread1与thread2的方法(11~14行)与ThreadTest1不同,ThreadTest2直接创建了一个Thread的对象,并将Myclass的对象作为参数传给Thread的构造方法。任何实现了Runnable接口的类的对象都可以作Thread构造方法的参数。在main()方法中,其余部分程序ThreadTest2与ThreadTest1.java相同。
ThreadTest2的MyClass类(31~59行)实现了接口Runnable,注意MyClass的构造方法实现了name这一类变量,实现run()时不需要调用Thread的方法getName(),但在实现randomWait()时要使用Thread.currentThread().Sleep()(39行),因为Runnable接口并未提供方法sleep(),因而实现时必须调用Thread的类方法currentThread()来调用sleep()。
事实上,无论用继承Thread的方法或用实现接口Runnable的方法来实现多线程,在程序书写时区别不大,只需概念清楚略加注意便可。使用继承Thread类的方法比较简单易懂,实现方便。但如果你创建的Thread需要是某个其它类的子类时,使用继承Thread的方法就会出麻烦。比如,实现Applet时,每个applet必须是java.applet.Applet的子类,此时想要实现多线程,只有通过使用Runnable接口,当然,使用Runnable接口来实现线程,在书写时会比较麻烦,因为你将不得不多做一些工作才可调用Thread的方法。
3.线程同步
在使用多线程时,由于可以共享资源,有时就会发生冲突。举一个简单的例子,有两个线程thread1负责写,thread2负责读,当它们操作同一个对象时,会发现由于thread1与thread2是同时执行的,因此可能thread1修改了数据而thread2读出的仍为旧数据,此时用户将无法获得预期的结果。问题之所以产生主要是由于资源使用协调不当(不同步)造成的。以前,这个问题一般由操作系统解决,而Java提供了自己协调资源的方法。
Java提供了同步方法和同步状态来协调资源。Java规定:被宣布为同步(使用Synchronized关键字)的方法,对象或类数据,在任何一个时刻只能被一个线程使用。通过这种方式使资源合理使用,达到线程同步的目的。
我们将程序5.1的类MyThread中的方法run()改成如下所示(见程序片段5.3),并加入一个类SynchronizedShow实现同步,大家可以运行看到执行结果:每次执行Show()的只有一个线程。
例5.3 ThreadTest3.java片段
public void run(){
int i=0;
while(keepRunning) i++;//i代表循环次数
//输出结果
SynchronizedShow.show(getName(),i);
SynchronizedShow.println(getName()+”is dead!”);
}
class SychronizedShow{
//方法show(String,int)被宣布为同步的方法,因此每次只有一个线程能调用这个方法
public static synchronized void show(String,name,int i){
int k;
k=i;
for(intj=0;j<=3;j++){
MyThread t=(Mythread) Thread.currentThread();
t.randomWait();
System.out.println(“I am”+name+”—— I have run”+k+” times.”);
k++;
}
}
}
运行结果(略)
另外,利用Synchronized可以锁定对象。
例如:Synchronized(某个对象A){
//程序块
}
在此程序块中,对于相同的对象A,在任何时候只可以有一个线程在此代码中执行,但对于不同的对象还是有很多个线程同时执行的。用同样的方法也可以协调类数据,例如:
Synchroinzed(new欲锁定的类().getmethod()){
//程序块
}
方法getmethod()是用来获取类数据的,这样通过利用Synchronized这一关键字,我们可以自由协调对象实体的各种数据。
除了简单使用Synchronized这一关键字外,Java有一套复杂的同步机制,其基本原理采用了C.A.R.Hoare提出的,并已被广泛使用的监视规则和条件变量规则。在Java中,所有的类与对象都和管程(monitor)联系在一起。当一个对象获得管程(monitor)时,它就可以执行同步方法,直至它让出管程(monitor),其它对象方可进入同步方法。当一个方法被完成时,它将自动让管理(monitor) ,另外执行某些方法例如wait(),suspend()等时也会让出管程(monitor)。
读者可以试着将例5.1的main()方法改成以下形式(风险5.4)执行一下。人们会发现与前面利用Synchronized的结果类似,原因是thread1执行suspend()后被挂起直至resume()方被唤醒。因此用这种方法也可以实现同步。
例5.4 Thread Test4.java的程序片断。
public static void main(String args[])
throws java.io.IOException{
System.out.println(“If want to show the result,press return”);
MyThread thread1=new MyThread(“thread1”);
MyThread thread2=new MyThread(“thread2”);
thread1.start();
thread2.start();
char ch;
while((ch=(char)System.in.read())!=’ ‘);
thread1.tStart();
//将线程thread1挂起
thread1.suspend();
thread2.tStart();
while(thread2.isAlive());
//线程thread2进入死亡状态后,释放线程thread1
thread1.resume();
while(thread1.isAlive());
/*{
you can do anything that you want to do here.
}
*/
System.out.println(“The test is end.”);
}
运行结果:(略)
4.进一步学习
上几节我们学习了定义Thread的两种方法,线程的同步的基本知识。在本节中我们将进一步给出有关线程的一些更详细的内容。
(1)若干常用的方法
首先,我们给出一些Thread类中最常用的方法供大学参考。
■currentThread()
这是一个类方法,返回当前正在执行的线程。
■isAlive()
判别一个线程是否仍然活着,包括这个线程正在执行或有机会被执行。返回一个布尔值。
■suspend()
悬挂起某个线程,并使得这个线程只可被resume()方法激活。
■resume()
与suspend()配合使用。唤醒线程。
■yeild()
强迫线程交出执行权利供其它线程使用。
■stop()
使线程进入死亡(dead)状态。
这些方法是线程中最常被使用到的方法,若要详细了解更多的内容可以查阅Java的API。
(2)线程优先级与调度
由于我我们一般使用的计算机是单CPU的,所以在执行多线程程序时需进行线程调度。线程调度是由线程的优先级决定的。高优先级的线程总是先运行的。Java采用的是抢占式(preemptive)的调度方式,即当高优先级的线程进入可运行(runnable)状态时,会抢占低优先级的线程的位置,并开始执行。当同时有两个或两个以上的线程具有高优先级并进入可运行状态,Java的调度会自动在这些线程间交替调度执行。
在Java中,Thread类中预定义了三个常量:
MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY,
一个线程的优先级应在MAX_PRIORITY与MIN_PRIORITY之间。NORM_PRIORITY是缺省的优先级值,一般是MIN_PRIORITY与MAX_PRIORITY的平均值。
在Java中,Thread类提供了方法设置和获取优先级。
setPriority(int)  用于设置线程的优先数
setPriority()   用于获取线程的优先数
(3)线程组(Thread Group)
线程组是包括了许多线程的对象集。每个线程有自己特定的线程组。一个线程在创建时就属于某个线程组,直至其执行结束,此线程不可更改其所属的线程组。Thread类中提供了构造方法使创建线程时同时决定其线程组。Thread类总共提供了六种构造方法:
Thread();
Thread(String);
Thread(Runnable);
Thread(Runnable,String);
Thread(ThreadGroup,String);
Thread(ThreadGroup,Runnable,String);
前四种缺省了线程组,表示所创建的线程属于main线程组,后两种则指定了所创建的线程的线程组。线程可以访问自己所在的线程组,但不能访问本线程组的父类。对线程组进行操作就是对线程组中的各个线程同时进行操作。
线程组的构造方法:
ThreadGroup(String groupName)
创建名为groupName的线程组,该线程组的父类为当前线程所在程组。
ThreadGroup(ThreadGroup parent,String groupName)
创建名为groupName的线程组,该线程组的父类是parent。
(4)wait()和notify()方法
Java在类java.lang.Object中定义了wait()和notify()方法,调用它们也可以实现线程之间的同步。
■public final void wait(long millseconds) throws InterruptedException
调用此方法时,被调对象进入等待状态,直到被唤醒或等待时间到。
■public final void notify()
唤醒一个对象内处于等待状态的对象。
■public find void Allotify()
唤醒一个对象内所有处于等待状态的对象。

5.2 Debugger的使用

  至此我们已经书写了不少程序,大家可能发觉出错要调试很困难,其实java工具包中的Java debugger为用户提供了方便的调试机制。虽然jdb的界面不是很漂亮,但很有用,尤其对于调试多线程程序。
Java的debugger需要jdb命令激活。如下执行:
C:>jdb
在执行之前,请用带-g参数的javac对程序进行编译,本节我们使用了前一章中check.java,第一步先重新编译方法如下:
C:synetjavajavaexmples>javac -g check.java
用下面的方法可以进入jdb,可以直接在jdb后紧接要调试的类名,也可缺省,在进入jdb后利用load载入要调试的类。
C:MyDemodawn>jdb Check
Initializing jdb…
0xe8d370:class(Check)
在Check这一类名前的16进制数是Check类在Java运行时的标识。
进入了jdb后,可以使用help来获取所需的使用信息:
> help
** command list **
run [class [args]]     – start execution of application’s main class

threads [threadgroup]   – list threads
thread      – set default thread
suspend [thread id(s)]   – suspend threads (default: all)
resume [thread id(s)]   – resume threads (default: all)
where [thread id] | all  – dump a thread’s stack
wherei [thread id] | all – dump a thread’s stack, with pc info
up [n frames]       – move up a thread’s stack
down [n frames]      – move down a thread’s stack
kill    – kill a thread with the given exception object
interrupt     – interrupt a thread

print        – print value of expression
dump         – print all object information
eval         – evaluate expression (same as print)
set =    – assign new value to field/variable/array element
locals           – print all local variables in current stack frame

classes          – list currently known classes
class      – show details of named class
methods     – list a class’s methods
fields     – list a class’s fields

threadgroups       – list threadgroups
threadgroup     – set current threadgroup

stop in .[(argument_type,…)]
— set a breakpoint in a method
stop at : — set a breakpoint at a line
clear .[(argument_type,…)]
— clear a breakpoint in a method
clear : — clear a breakpoint at a line
clear          – list breakpoints
catch     – break when specified exception thrown
ignore     – cancel ‘catch’ for the specified exception
watch [access|all] .
— watch access/modifications to a field
unwatch [access|all] .
— discontinue watching access/modifications to a field
trace methods [thread] — trace method entry and exit
untrace methods [thread] — stop tracing method entry and exit
step           – execute current line
step up         – execute until the current method returns to its cal
ler
stepi          – execute current instruction
next           – step one line (step OVER calls)
cont           – continue execution from breakpoint

list [line number|method] — print source code
use (or sourcepath) [source file path]
— display or change the source path
exclude [class id … | “none”]
— do not report step or method events for specified classes
classpath — print classpath info from target VM

monitor    – execute command each time the program stops
  monitor        – list monitors
  unmonitor <monitor#> – delete a monitor
  read     – read and execute a command file

lock       – print lock info for an object
threadlocks [thread id] — print lock info for a thread

disablegc     – prevent garbage collection of an object
enablegc      – permit garbage collection of an object

!!            – repeat last command
      – repeat command n times
  help (or ?)       – list commands
  version         – print version information
  exit (or quit)      – exit debugger

: full class name with package qualifiers or a
pattern with a leading or trailing wildcard (‘*’).
: thread number as reported in the ‘threads’ command
: a Java(tm) Programming Language expression.
Most common syntax is supported.

Startup commands can be placed in either “jdb.ini” or “.jdbrc”
in user.home or user.dir
>
利用命令stop in .可以在方法中设置断点,用命令run运行方法。
>stop in Check.main
Breakpoint set Check.main
>run Check
running…
main[1]
Breakpoint hit: Check.main(Chech:18)
main[1] list
14   }
15  }
16  class Check{
17   public static void main(String args[]]){
18  =>  Son s=new Son();
19     s.speak();
20   }
21   }
main[1]
=>所指的地方即为断点处,然后使用step命令进行步调执行,结果提示断点设置到了类son中用list显示,就可以清楚地看到断点所在位置。
main[1]step
main[1]
Breakpoint hit:Son.(Son:9)
main[1]list
5    void speak(String s){
6      System.out.println(“I like “+s+”.”);
7    }
8   }
9   =>class Son extends Father{
10   void speak(){
11      System.out.println(“My father sys:”);
12      super.speak();
13      super.speak(“hunting”);
用cont命令可以继续执行下去,本例十分简单,所以立即给出了运行结果。我们还可以试一试help中显示的其它命令,methods check显示了check类中定义的方法,memory显示了java运行时刻提供的内存等等,用户可以自己去试用一下。
main[1]cont
My father syas:main[1]
I am Father.
I like hunting.
//显示check类中定义的方法
main[1]methods Check
void main(String[])
void ()
//显示java运行时刻提供的内存main[1] memory
Free:295928,total:1777656
//列出线程组
main[1]threadgrouts
1.(java.lang.ThreadGroup)0xe600b8 system
2.(java.lang.ThreadGroup)0xe655e0 main
3.(java.lang.ThreadGroup)0xe8ddd8 Check.main
//列出指定线程组中的线程
main[1]threads system
Group system:
1.(java.lang.Thread)0xe600d0  Frinalizer therad suspended
2.(java.lang.Thread)0xe65570  Debugger agent running
3.(sun.tools.debug.BreakpointHandler)0xe8b080 Breakpoint handler cond. waitin
Group main:
4.(java.lang.Thread)0xe600a8 main suspended
Group Check.main:
事实上,Jdb工具现在还存在不少缺陷正待改进,使用时有时会出现一些莫名其妙的错误,所以使用时请大家最好联网,便于查询。

发表评论

电子邮件地址不会被公开。 必填项已用*标注