第五章線程與異常處理(上)
上一章我們介紹了Java的面向?qū)ο蟮臋C制:類、包和接口。本章我們將介紹一下Java的另外兩個機制:多線程(Multithread)和異常處理(Exception)。本章前半部分是關(guān)于Thread這一基本類以及一套先進的同步原語的介紹,它們使得利用Java編寫多線程大為方便。在本章的后半部分我們將介紹Java的異常處理機制(Exception),異常處理機制提高了程序的健壯性。另外,本章中間將介紹一個Java的debugger工具Jdb的使用,Jdb工具對于調(diào)試多線程程序尤其有好處。
5.1 多線程(Multithread)
5.1.1 線程的基本概念
在介紹多線程之前,我們先來了解一些相關(guān)的基本概念。一般來說,我們把程序的一次執(zhí)行稱為進程(process)。一個進程包括一個程序模塊和該模塊一次執(zhí)行時所處理的數(shù)據(jù)。每個進程與其它進程擁有不同的數(shù)據(jù)塊,其內(nèi)存地址是分開的。進程之間的通信要通過尋址,一般需使用信號、管道等進行通信。線程(thread)是指進程內(nèi)部一段可獨立執(zhí)行的有獨立控制流的指令序列。子線程與其父線程共享一個地址空間,同一個任務(wù)中的不同線程共享任務(wù)的各項資源。
多進程與多線程是多任務(wù)的兩種類型。以前的操作系統(tǒng),如Win31,只運行多進程,而Win95及WinNT則支持多線程與多進程。Java通過提供Package類(Java.lang.package)支持多進程,而提供Thread類來支持多線程。
多線程與多進程的主要區(qū)別在于,線程是一個進程中一段獨立的控制流,一個進程可以擁有若干個線程。在多進程設(shè)計中各個進程之間的數(shù)據(jù)塊是相互獨立的,一般彼此不影響,要通過信號、管道等進行交流。而在多線程設(shè)計中,各個線程不一定獨立,同一任務(wù)中的各個線程共享程序段、數(shù)據(jù)段等資源,如圖5.1。
正如字面上所表述的那樣,多線程就是同時有多個線程在執(zhí)行。在多CPU的計算機中,多線程的實現(xiàn)是真正的物理上的同時執(zhí)行。而對于單CPU的計算機而言,實現(xiàn)的只是邏輯上的同時執(zhí)行。在每個時刻,真正執(zhí)行的只有一個線程,由操作系統(tǒng)進行線程管理調(diào)度,但由于CPU的速度很快,讓人感到像是多個線程在同時執(zhí)行。
多線程比多進程更方便于共享資源,而Java又提供了一套先進的同步原語解決線程之間的同步問題,使得多線程設(shè)計更易發(fā)揮作用。用Java設(shè)計動畫以及設(shè)計多媒體應(yīng)用實例時會廣泛地使用到多線程,在后面幾章你將看到多線程的巨大作用,當(dāng)然,現(xiàn)在必須先學(xué)習(xí)一些多線程的基本知識,慢慢地你就體會到它的優(yōu)越性。
5.1.2 線程的狀態(tài)
如同進程有等待、運行、就緒等狀態(tài)一樣,線程也有其狀態(tài)。
當(dāng)一個線程通過new被創(chuàng)建但還未運行時,稱此線程處于準(zhǔn)備狀態(tài)(new狀態(tài))。當(dāng)線程調(diào)用了start()方法或執(zhí)行run()方法后,則線程處于可運行狀態(tài)。若在等待與其它線程共享資源,則稱線程處于等待狀態(tài)。線程的另一個狀態(tài)稱為不可運行(notrunnable)狀態(tài),此時線程不僅等分享處理器資源,而且在等待某個能使它返回可運行狀態(tài)的事件,例如被方法suspend()掛起的進程就要等待方法resume()方可被喚醒。當(dāng)調(diào)用了stop()方法或線程執(zhí)行完畢,則線程進入死亡(dead)狀態(tài)。線程的各個狀態(tài)之間的轉(zhuǎn)換關(guān)系見圖5.2。
5.1.3 創(chuàng)建線程
在了解基本概念后,下面學(xué)習(xí)如何在Java中創(chuàng)建多線程。
Java通過java.lang.Thread類來支持多線程。在Thread類中封裝了獨立的有關(guān)線程執(zhí)行的數(shù)據(jù)和方法,并將多線程與面向?qū)ο蟮慕Y(jié)構(gòu)合為一體。
Java提供了兩種方法創(chuàng)建線程,一種是繼承Thread類,另一種則是實現(xiàn)接口Runnable。
1.繼承Thread類
通過繼承Thread類創(chuàng)建線程十分簡單,只需要重載run()方法提供執(zhí)行入口就可以,下面我們通過例5.1來解釋說明。
例5.1 ThreadTest1.java。
運行結(jié)果:(略)
ThreadTest2創(chuàng)建thread1與thread2的方法(11~14行)與ThreadTest1不同,ThreadTest2直接創(chuàng)建了一個Thread的對象,并將Myclass的對象作為參數(shù)傳給Thread的構(gòu)造方法。任何實現(xiàn)了Runnable接口的類的對象都可以作Thread構(gòu)造方法的參數(shù)。在main()方法中,其余部分程序ThreadTest2與ThreadTest1.java相同。
ThreadTest2的MyClass類(31~59行)實現(xiàn)了接口Runnable,注意MyClass的構(gòu)造方法實現(xiàn)了name這一類變量,實現(xiàn)run()時不需要調(diào)用Thread的方法getName(),但在實現(xiàn)randomWait()時要使用Thread.currentThread().Sleep()(39行),因為Runnable接口并未提供方法sleep(),因而實現(xiàn)時必須調(diào)用Thread的類方法currentThread()來調(diào)用sleep()。
事實上,無論用繼承Thread的方法或用實現(xiàn)接口Runnable的方法來實現(xiàn)多線程,在程序書寫時區(qū)別不大,只需概念清楚略加注意便可。使用繼承Thread類的方法比較簡單易懂,實現(xiàn)方便。但如果你創(chuàng)建的Thread需要是某個其它類的子類時,使用繼承Thread的方法就會出麻煩。比如,實現(xiàn)Applet時,每個applet必須是java.applet.Applet的子類,此時想要實現(xiàn)多線程,只有通過使用Runnable接口,當(dāng)然,使用Runnable接口來實現(xiàn)線程,在書寫時會比較麻煩,因為你將不得不多做一些工作才可調(diào)用Thread的方法。
3.線程同步
在使用多線程時,由于可以共享資源,有時就會發(fā)生沖突。舉一個簡單的例子,有兩個線程thread1負責(zé)寫,thread2負責(zé)讀,當(dāng)它們操作同一個對象時,會發(fā)現(xiàn)由于thread1與thread2是同時執(zhí)行的,因此可能thread1修改了數(shù)據(jù)而thread2讀出的仍為舊數(shù)據(jù),此時用戶將無法獲得預(yù)期的結(jié)果。問題之所以產(chǎn)生主要是由于資源使用協(xié)調(diào)不當(dāng)(不同步)造成的。以前,這個問題一般由操作系統(tǒng)解決,而Java提供了自己協(xié)調(diào)資源的方法。
Java提供了同步方法和同步狀態(tài)來協(xié)調(diào)資源。Java規(guī)定:被宣布為同步(使用Synchronized關(guān)鍵字)的方法,對象或類數(shù)據(jù),在任何一個時刻只能被一個線程使用。通過這種方式使資源合理使用,達到線程同步的目的。
我們將程序5.1的類MyThread中的方法run()改成如下所示(見程序片段5.3),并加入一個類SynchronizedShow實現(xiàn)同步,大家可以運行看到執(zhí)行結(jié)果:每次執(zhí)行Show()的只有一個線程。
例5.3 ThreadTest3.java片段
public void run(){
int i=0;
while(keepRunning) i++;//i代表循環(huán)次數(shù)
//輸出結(jié)果
SynchronizedShow.show(getName(),i);
SynchronizedShow.println(getName()+"isdead!");
}
class SychronizedShow{
//方法show(String,int)被宣布為同步的方法,因此每次只有一個線程能調(diào)用這個方法
public static synchronized voidshow(String,name,int i){
int k;
k=i;
for(intj=0;j<=3;j++){
MyThread t=(Mythread)Thread.currentThread();
t.randomWait();
System.out.println("Iam"+name+"—— I haverun"+k+" times.");
k++;
}
}
}
運行結(jié)果(略)
另外,利用Synchronized可以鎖定對象。
例如:Synchronized(某個對象A){
//程序塊
}
在此程序塊中,對于相同的對象A,在任何時候只可以有一個線程在此代碼中執(zhí)行,但對于不同的對象還是有很多個線程同時執(zhí)行的。用同樣的方法也可以協(xié)調(diào)類數(shù)據(jù),例如:
Synchroinzed(new欲鎖定的類().getmethod()){
//程序塊
}
方法getmethod()是用來獲取類數(shù)據(jù)的,這樣通過利用Synchronized這一關(guān)鍵字,我們可以自由協(xié)調(diào)對象實體的各種數(shù)據(jù)。
除了簡單使用Synchronized這一關(guān)鍵字外,Java有一套復(fù)雜的同步機制,其基本原理采用了C.A.R.Hoare提出的,并已被廣泛使用的監(jiān)視規(guī)則和條件變量規(guī)則。在Java中,所有的類與對象都和管程(monitor)聯(lián)系在一起。當(dāng)一個對象獲得管程(monitor)時,它就可以執(zhí)行同步方法,直至它讓出管程(monitor),其它對象方可進入同步方法。當(dāng)一個方法被完成時,它將自動讓管理(monitor),另外執(zhí)行某些方法例如wait(),suspend()等時也會讓出管程(monitor)。
讀者可以試著將例5.1的main()方法改成以下形式(風(fēng)險5.4)執(zhí)行一下。人們會發(fā)現(xiàn)與前面利用Synchronized的結(jié)果類似,原因是thread1執(zhí)行suspend()后被掛起直至resume()方被喚醒。因此用這種方法也可以實現(xiàn)同步。
例5.4 Thread Test4.java的程序片斷。
public static void main(String args[])
throws java.io.IOException{
System.out.println("If want toshow the result,press return");
MyThread thread1=newMyThread("thread1");
MyThread thread2=newMyThread("thread2");
thread1.start();
thread2.start();
char ch;
while((ch=(char)System.in.read())!=‘\n‘);
thread1.tStart();
//將線程thread1掛起
thread1.suspend();
thread2.tStart();
while(thread2.isAlive());
//線程thread2進入死亡狀態(tài)后,釋放線程thread1
thread1.resume();
while(thread1.isAlive());
/*{
you can do anything that youwant to do here.
}
*/
System.out.println("The testis end.");
}
運行結(jié)果:(略)
4.進一步學(xué)習(xí)
上幾節(jié)我們學(xué)習(xí)了定義Thread的兩種方法,線程的同步的基本知識。在本節(jié)中我們將進一步給出有關(guān)線程的一些更詳細的內(nèi)容。
(1)若干常用的方法
首先,我們給出一些Thread類中最常用的方法供大學(xué)參考。
■currentThread()
這是一個類方法,返回當(dāng)前正在執(zhí)行的線程。
■isAlive()
判別一個線程是否仍然活著,包括這個線程正在執(zhí)行或有機會被執(zhí)行。返回一個布爾值。
■suspend()
懸掛起某個線程,并使得這個線程只可被resume()方法激活。
■resume()
與suspend()配合使用。喚醒線程。
■yeild()
強迫線程交出執(zhí)行權(quán)利供其它線程使用。
■stop()
使線程進入死亡(dead)狀態(tài)。
這些方法是線程中最常被使用到的方法,若要詳細了解更多的內(nèi)容可以查閱Java的API。
(2)線程優(yōu)先級與調(diào)度
由于我我們一般使用的計算機是單CPU的,所以在執(zhí)行多線程程序時需進行線程調(diào)度。線程調(diào)度是由線程的優(yōu)先級決定的。高優(yōu)先級的線程總是先運行的。Java采用的是搶占式(preemptive)的調(diào)度方式,即當(dāng)高優(yōu)先級的線程進入可運行(runnable)狀態(tài)時,會搶占低優(yōu)先級的線程的位置,并開始執(zhí)行。當(dāng)同時有兩個或兩個以上的線程具有高優(yōu)先級并進入可運行狀態(tài),Java的調(diào)度會自動在這些線程間交替調(diào)度執(zhí)行。
在Java中,Thread類中預(yù)定義了三個常量:
MAX_PRIORITY,MIN_PRIORITY,NORM_PRIORITY,
一個線程的優(yōu)先級應(yīng)在MAX_PRIORITY與MIN_PRIORITY之間。NORM_PRIORITY是缺省的優(yōu)先級值,一般是MIN_PRIORITY與MAX_PRIORITY的平均值。
在Java中,Thread類提供了方法設(shè)置和獲取優(yōu)先級。
setPriority(int) 用于設(shè)置線程的優(yōu)先數(shù)
setPriority() 用于獲取線程的優(yōu)先數(shù)
(3)線程組(Thread Group)
線程組是包括了許多線程的對象集。每個線程有自己特定的線程組。一個線程在創(chuàng)建時就屬于某個線程組,直至其執(zhí)行結(jié)束,此線程不可更改其所屬的線程組。Thread類中提供了構(gòu)造方法使創(chuàng)建線程時同時決定其線程組。Thread類總共提供了六種構(gòu)造方法:
Thread();
Thread(String);
Thread(Runnable);
Thread(Runnable,String);
Thread(ThreadGroup,String);
Thread(ThreadGroup,Runnable,String);
前四種缺省了線程組,表示所創(chuàng)建的線程屬于main線程組,后兩種則指定了所創(chuàng)建的線程的線程組。線程可以訪問自己所在的線程組,但不能訪問本線程組的父類。對線程組進行操作就是對線程組中的各個線程同時進行操作。
線程組的構(gòu)造方法:
ThreadGroup(String groupName)
創(chuàng)建名為groupName的線程組,該線程組的父類為當(dāng)前線程所在程組。
ThreadGroup(ThreadGroup parent,StringgroupName)
創(chuàng)建名為groupName的線程組,該線程組的父類是parent。
(4)wait()和notify()方法
Java在類java.lang.Object中定義了wait()和notify()方法,調(diào)用它們也可以實現(xiàn)線程之間的同步。
■public final void wait(longmillseconds) throws InterruptedException
調(diào)用此方法時,被調(diào)對象進入等待狀態(tài),直到被喚醒或等待時間到。
■public final void notify()
喚醒一個對象內(nèi)處于等待狀態(tài)的對象。
■public find void Allotify()
喚醒一個對象內(nèi)所有處于等待狀態(tài)的對象。
5.2 Debugger的使用
至此我們已經(jīng)書寫了不少程序,大家可能發(fā)覺出錯要調(diào)試很困難,其實java工具包中的Javadebugger為用戶提供了方便的調(diào)試機制。雖然jdb的界面不是很漂亮,但很有用,尤其對于調(diào)試多線程程序。
Java的debugger需要jdb命令激活。如下執(zhí)行:
C:\>jdb
在執(zhí)行之前,請用帶-g參數(shù)的javac對程序進行編譯,本節(jié)我們使用了前一章中check.java,第一步先重新編譯方法如下:
C:\synetjava\java\exmples>javac -gcheck.java
用下面的方法可以進入jdb,可以直接在jdb后緊接要調(diào)試的類名,也可缺省,在進入jdb后利用load載入要調(diào)試的類。
C:\MyDemo\dawn>jdb Check
Initializing jdb...
0xe8d370:class(Check)
在Check這一類名前的16進制數(shù)是Check類在Java運行時的標(biāo)識。
進入了jdb后,可以使用help來獲取所需的使用信息:
> help
** command list **
run [class [args]] --start execution of application‘s main class
threads [threadgroup] --list threads
thread <thread id> --set default thread
suspend [thread id(s)] --suspend threads (default: all)
resume [thread id(s)] --resume threads (default: all)
where [thread id] | all -- dump athread‘s stack
wherei [thread id] | all -- dump athread‘s stack, with pc info
up [n frames] --move up a thread‘s stack
down [n frames] --move down a thread‘s stack
kill <thread> <expr> --kill a thread with the given exceptionobject
interrupt <thread> --interrupt a thread
print <expr> --print value of expression
dump <expr> --print all object information
eval <expr> --evaluate expression (same as print)
set <lvalue> = <expr> --assign new value to field/variable/arrayelement
locals --print all local variables in current stackframe
classes --list currently known classes
class <class id> --show details of named class
methods <class id> --list a class‘s methods
fields <class id> --list a class‘s fields
threadgroups --list threadgroups
threadgroup <name> --set current threadgroup
stop in <classid>.<method>[(argument_type,...)]
--set a breakpoint in a method
stop at <class id>:<line>-- set a breakpoint at a line
clear <classid>.<method>[(argument_type,...)]
--clear a breakpoint in a method
clear <class id>:<line> --clear a breakpoint at a line
clear --list breakpoints
catch <class id> --break when specified exception thrown
ignore <class id> --cancel ‘catch‘ for the specified exception
watch [access|all] <classid>.<field name>
--watch access/modifications to a field
unwatch [access|all] <classid>.<field name>
--discontinue watching access/modifications toa field
trace methods [thread] -- trace methodentry and exit
untrace methods [thread] -- stoptracing method entry and exit
step --execute current line
step up --execute until the current method returns toits cal
ler
stepi --execute current instruction
next --step one line (step OVER calls)
cont --continue execution from breakpoint
list [line number|method] -- printsource code
use (or sourcepath) [source file path]
--display or change the source path
exclude [class id ... |"none"]
--do not report step or method events forspecified classes
classpath -- print classpath info fromtarget VM
monitor <command> --execute command each time the program stops
monitor -- listmonitors
unmonitor <monitor#> --delete a monitor
read <filename> --read and execute a command file
lock <expr> --print lock info for an object
threadlocks [thread id] -- print lockinfo for a thread
disablegc <expr> --prevent garbage collection of an object
enablegc <expr> --permit garbage collection of an object
!! --repeat last command
<n> <command> --repeat command n times
help (or ?) --list commands
version --print version information
exit (or quit) --exit debugger
<class id>: full class namewith package qualifiers or a
pattern with a leading or trailingwildcard (‘*‘).
<thread id>: thread number asreported in the ‘threads‘ command
<expr>: a Java(tm) ProgrammingLanguage expression.
Most common syntax is supported.
Startup commands can be placed ineither "jdb.ini" or".jdbrc"
in user.home or user.dir
>
利用命令stop in <classid>.<method>可以在方法中設(shè)置斷點,用命令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 voidmain(String args[]]){
18 => Son s=new Son();
19 s.speak();
20 }
21 }
main[1]
=>所指的地方即為斷點處,然后使用step命令進行步調(diào)執(zhí)行,結(jié)果提示斷點設(shè)置到了類son中用list顯示,就可以清楚地看到斷點所在位置。
main[1]step
main[1]
Breakpoint hit:Son.<init>(Son:9)
main[1]list
5 void speak(String s){
6 System.out.println("Ilike "+s+".");
7 }
8 }
9 =>class Son extendsFather{
10 void speak(){
11 System.out.println("Myfather sys:");
12 super.speak();
13 super.speak("hunting");
用cont命令可以繼續(xù)執(zhí)行下去,本例十分簡單,所以立即給出了運行結(jié)果。我們還可以試一試help中顯示的其它命令,methodscheck顯示了check類中定義的方法,memory顯示了java運行時刻提供的內(nèi)存等等,用戶可以自己去試用一下。
main[1]cont
My father syas:main[1]
I am Father.
I like hunting.
//顯示check類中定義的方法
main[1]methods Check
void main(String[])
void <init>()
//顯示java運行時刻提供的內(nèi)存main[1]memory
Free:295928,total:1777656
//列出線程組
main[1]threadgrouts
1.(java.lang.ThreadGroup)0xe600b8system
2.(java.lang.ThreadGroup)0xe655e0 main
3.(java.lang.ThreadGroup)0xe8ddd8Check.main
//列出指定線程組中的線程
main[1]threads system
Group system:
1.(java.lang.Thread)0xe600d0 Frinalizertherad suspended
2.(java.lang.Thread)0xe65570 Debuggeragent running
3.(sun.tools.debug.BreakpointHandler)0xe8b080Breakpoint handler cond. waitin
Group main:
4.(java.lang.Thread)0xe600a8 mainsuspended
Group Check.main:
事實上,Jdb工具現(xiàn)在還存在不少缺陷正待改進,使用時有時會出現(xiàn)一些莫名其妙的錯誤,所以使用時請大家最好聯(lián)網(wǎng),便于查詢。