当前位置 : 李杰的流水账 > 技术 > linux

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工具现在还存在不少缺陷正待改进,使用时有时会出现一些莫名其妙的错误,所以使用时请大家最好联网,便于查询。

内容列表