Java的线程处理课件_第1页
Java的线程处理课件_第2页
Java的线程处理课件_第3页
Java的线程处理课件_第4页
Java的线程处理课件_第5页
已阅读5页,还剩106页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Java的線程處理 10.1線程的基本概念

我們已經對多任務非常熟悉,Windows和Linux都是多任務的操作系統。這些操作系統可以同時運行兩個或兩個以上的程式,並且看起來這些程式似乎在同時運行。當然,除非你的電腦擁有多個處理器,否則這些程式是不可能同時運行的。操作系統負責把系統資源分配給這些運行中的程式,並讓人感覺它們是併發活動的。圖10.1顯示了支持多任務的操作系統和不支持多任務的操作系統運行程式的情況。圖10.1

實現多任務通常有兩種方法,一種稱為搶佔式多任務(preemptivemultitasking);一種叫合作式多任務(cooperativemultitasking)。對於搶佔式多任務,操作系統自行決定何時中斷一個程式,將執行時間分給其他程式。相反,對於合作式多任務操作系統將與程式進行協商,只有程式自願放棄控制時才被中斷。雖然搶佔式多任務實現起來困難一些,但卻有效得多。對於合作式多任務來說,一個運行不好的程式會佔有整個系統。

多線程把操作系統的多任務原理應用到程式中,進一步發展了這一原理。應用了多線程技術的程式如同多任務操作系統一樣,可以同時執行多個任務。每個任務被稱為一個線程——它是線程式控制制流的簡稱。實際上,多線程的應用非常廣泛,例如,流覽器在下載數據的同時還可以流覽其他網頁,或者當某個網頁下載太慢時,還可以控制流覽器中止這個網頁流覽。Java語言本身也使用一個線程在後臺收集無用的記憶體單元——這樣就減少了用戶管理記憶體的麻煩!

通常,我們把操作系統的多個任務稱為進程(Process),而程式中的多任務則稱為線程。那麼,線程和進程之間有什麼區別呢?最基本的區別就是每個進程都擁有一組完整的屬於自己的變數,而線程則共用這些數據。看起來這樣似乎不如進程安全,確實如此,本章後面將會更詳細地討論。但線程的優勢在於創建和註銷線程的開銷比運行新的進程少得多,所以現在主流的操作系統都支持多線程。而且,和進程間的通信相比,線程間的通信要快得多,也方便得多。10.1.1線程不少程式語言都提供對線程的支持,同這些語言相比,Java的特點是從最底層開始就對線程提供支持。除此以外,標準的Java類是可重載的,它允許在一個給定的應用程式中由多個線程調用同一方法,而線程彼此之間又互不干擾。Java的這些特點為多線程應用程式的設計奠定了基礎。究竟什麼是線程呢?正如圖10.2中所示,一個線程是給定的指令的序列(你所編寫的代碼)、一個棧(在給定的方法中定義的變數),以及一些共用數據(類一級的變數)。線程也可以從全局類中訪問靜態數據。圖10.2

每個線程都有其自己的堆疊和程式計數器(PC)。用戶可以把程式計數器(PC)設想為用於跟蹤線程正在執行的指令,而堆疊用於跟蹤線程的上下文(上下文是當線程執行到某處時,當前的局部變數的值)。雖然用戶可以編寫出線上程之間傳送數據的副程式,但在正常情況下,一個線程不能訪問另外一個線程的棧變數。

一個線程或執行上下文由三個主要部分組成:

①一個虛擬處理機

②CPU執行的代碼

③代碼操作的數據代碼可以或不可以由多個線程共用,這和數據是獨立的。兩個線程如果執行同一個類的實例代碼,則它們可以共用相同的代碼。

類似地,數據可以或不可以由多個線程共用,這和代碼是獨立的。兩個線程如果共用對一個公共對象的存取,則它們可以共用相同的數據。在Java編程中,虛擬處理機封裝在Thread類的一個實例裏。構造線程時,定義其上下文的代碼和數據是由傳遞給它的構造函數的對象指定的。10.1.2創建線程在Java平臺中,創建一個線程非常簡單,最直接的方法就是從線程類java.lang.Thread繼承。在缺省情況下,線程類可以被所有的Java應用程式調用。為了使用線程類,我們需要瞭解Thejava.lang.Thread類中定義的五個方法:●run():該方法用於線程的執行。你需要重載該方法,以便讓線程做特定的工作。●start():該方法使得線程啟動run()方法。●stop():該方法同start()方法的作用相反,用於停止線程的運行。●suspend():該方法同stop()方法不同的是,它並不終止未完成的線程,而只是掛起線程,以後還可恢復。●resume():該方法重新啟動已經掛起的線程。下麵我們看一個通過派生Thread類來創建線程的實例。例10.1TestThreads.javapublicclassTestThreads{ publicstaticvoidmain(Stringargs[]) { MyThreada=newMyThread("ThreadA"); MyThreadb=newMyThread("ThreadB"); MyThreadc=newMyThread("ThreadC"); a.start(); b.start(); c.start();}}classMyThreadextendsThread{ Stringwhich; MyThread(Stringwhich) { this.which=which; }

publicvoidrun(){

intiterations=(int)(Math.random()*100)%15; intsleepinterval=(int)(Math.random()*1000); System.out.println(which+"runningfor"+ iterations+"iterations"); System.out.println(which+"sleepingfor"+

sleepinterval+"msbetweenloops"); for(inti=0;i<iterations;i++) { System.out.println(which+""+i); try { Thread.sleep(sleepinterval); } catch(InterruptedExceptione) {} }}}這個例子演示了如何從現有的Thread類中派生出一個新類。

新創建的類重載了run()方法,但實現run()方法不必很嚴格,因為Thread類可提供一個缺省的run()方法,儘管它不是特別有用。其運行結果如下:

ThreadArunningfor2iterationsThreadAsleepingfor913msbetweenloopsThreadA0ThreadBrunningfor12iterationsThreadBsleepingfor575msbetweenloopsThreadB0ThreadCrunningfor4iterationsThreadCsleepingfor370msbetweenloopsThreadC0ThreadC1ThreadB1ThreadC2ThreadA1ThreadC3ThreadB2ThreadB3ThreadB4ThreadB5ThreadB6ThreadB7ThreadB8ThreadB9ThreadB10ThreadB1110.1.3使用Runnable介面在不少場合,你不能重新定義類的父母,或者不能定義派生的線程類,但也許你的類的層次要求父類為特定的類,然而,Java語言是不支持多父類的。在這些情況下,可以通過Runnable介面來實現多線程的功能。實際上,Thread類本身也實現了Runnable介面。一個Runnable介面提供了一個publicvoidrun()方法。下麵我們來看一個用Runnable介面創建線程的實例。例10.2RunnableTest.javapublicclassRunnableTest{ publicstaticvoidmain(Stringargs[]) { Testr=newTest(); Threadt=newThread(r); t.start(); }}classTestimplementsRunnable{ inti;

publicvoidrun() { while(true) { System.out.println("Hello"+i++); if(i==10) break;} }}

上面程式的運行結果非常簡單,這裏不再列出。使用Runnable介面,需要我們實現run()方法。我們也需要創建Thread對象的一個實例,它最終是用來調用run()方法的。首先,main()方法構造了Test類的一個實例r。實例r有它自己的數據,在這裏就是整數i。因為實例r是傳給Thread的類構造函數的,所以r的整數i就是線程運行時刻所操作的數據。線程總是從它所裝載的Runnable實例(在本例中,這個實例就是r)的run()方法開始運行。

一個多線程編程環境允許創建基於同一個Runnable實例的多個線程。這可以通過以下方法來做到:

Threadt1=newThread(r); Threadt2=newThread(r);

此時,這兩個線程共用數據和代碼。10.1.4方法的選擇以上例子雖然展示了如何使用Runnable介面創建一個線程,但是它並不典型。我們說過,使用Runnable結構的主要原因是必須從其他父類繼承。那麼,什麼時候才是使用Runnable介面的最佳時機呢。給定各種方法的選擇,你如何決定使用哪個?下麵分別列出了選用這兩種方法的幾個原則。

使用Runnable的原因:●從面向對象的角度來看,Thread類是一個虛擬處理機嚴格的封裝,因此只有當處理機模型修改或擴展時,才應該繼承類。正因為這個原因和區別一個正在運行的線程的處理機、代碼和數據部分的意義,本教程採用了這種方法。●由於Java技術只允許單一繼承,所以如果你已經繼承了Thread,你就不能再繼承其他任何類,例如Applet。在某些情況下,這會使你只能採用實現Runnable的方法。●因為有時你必須實現Runnable,所以你可能喜歡保持一致,並總是使用這種方法。繼承Thread的優點:●當一個run()方法體現在繼承Thread類的類中,用this指向實際控制運行的Thread實例。因此代碼簡單了一些,許多Java編程語言的程式員使用擴展Thread的機制。注:如果你採用這種方法,在你的代碼生命週期的後期,單繼承模型可能會給你帶來困難。下麵的例子中分別使用了兩種方式創建線程,大家可以分析一下原因,以進一步理解如何使用這兩個線程模型。例10.3TimerTest.javaimportjava.awt.*;importjava.awt.event.*;importjavax.swing.*;importjava.util.*;

publicclassTimerTest{publicstaticvoidmain(String[]args){JFramef=newTimerTestFrame();f.show();}}

classTimerTestFrameextendsJFrame{publicTimerTestFrame(){ setSize(450,300);setTitle("TimerTest");

addWindowListener(newWindowAdapter(){ publicvoidwindowClosing(WindowEvente){ System.exit(0);}});

Containerc=getContentPane();c.setLayout(newGridLayout(2,3));c.add(newClockCanvas("SanJose","GMT-8"));c.add(newClockCanvas("Taipei","GMT+8"));c.add(newClockCanvas("Berlin","GMT+1"));c.add(newClockCanvas("NewYork","GMT-5"));c.add(newClockCanvas("Cairo","GMT+2"));c.add(newClockCanvas("Bombay","GMT+5"));

}}interfaceTimerListener{

voidtimeElapsed(Timert);}classTimerextendsThread{privateTimerListenertarget;privateintinterval;

publicTimer(inti,TimerListenert){ target=t;interval=i;setDaemon(true);}

publicvoidrun(){ try{ while(!interrupted()){ sleep(interval);target.timeElapsed(this);}}

catch(InterruptedExceptione){}}}

classClockCanvasextendsJPanelimplementsTimerListener{

privateintseconds=0;privateStringcity;privateintoffset;privateGregorianCalendarcalendar;privatefinalintLOCAL=16;

publicClockCanvas(Stringc,Stringtz){ city=c;calendar=newGregorianCalendar(TimeZone.getTimeZone(tz));Timert=newTimer(1000,this);t.start();setSize(125,125);}

publicvoidpaintComponent(Graphicsg){ super.paintComponent(g);g.drawOval(0,0,100,100);doublehourAngle=2*Math.PI*(seconds-3*60*60)/(12*60*60);doubleminuteAngle=2*Math.PI*(seconds-15*60)/(60*60);doublesecondAngle=2*Math.PI*(seconds-15)/60;g.drawLine(50,50,50+(int)(30*Math.cos(hourAngle)),50+(int)(30*Math.sin(hourAngle)));g.drawLine(50,50,50+(int)(40*Math.cos(minuteAngle)),50+(int)(40*Math.sin(minuteAngle)));g.drawLine(50,50,50+(int)(45*Math.cos(secondAngle)),50+(int)(45*Math.sin(secondAngle)));g.drawString(city,0,115);}

publicvoidtimeElapsed(Timert){ calendar.setTime(newDate());seconds=calendar.get(Calendar.HOUR)*60*60+calendar.get(Calendar.MINUTE)*60+calendar.get(Calendar.SECOND);

repaint();}}

這個例子實現了一個多國時間的現實窗口。程式中,Timer類是直接從Thread類繼承的,而ClockCanvas類是通過實現Runnable介面來實現線程的功能的。顯然,這是因為ClockCanvas類必須從JPanel類繼承用來畫出時鐘。程式的運行結果如圖10.3所示。圖10.310.2線程的屬性10.2.1線程的狀態線程有四種狀態,分別為●new(初始態):一個線程在調用new()方法之後,調用start()方法之前所處的狀態。在初始態中,可以調用start()和stop()方法。●rRunnable(可運行狀態):一旦線程調用了start()方法,線程就轉到Runnable()狀態。注意,如果線程處於Runnable狀態,它也有可能不在運行,這是因為還存在優先順序和調度問題。●blocked(阻塞/掛起狀態):線程處於阻塞狀態。這是由兩種可能性造成的:因掛起而暫停;由於某些原因而阻塞,例如等待IO請求的完成等。●dead(終止狀態):線程轉到退出狀態。這有兩種可能性:run()方法執行結束;調用了stop()方法。

一個Thread對象在它的生命週期中會處於各種不同的狀態。圖10.4形象地說明了這點。儘管線程變為可運行的,但它並不立即開始運行。在一個只帶有一個處理機的機器上,某一個時刻只能進行一個動作。在Java中,線程是搶佔式的,但並不一定是分時的(一個常見的概念錯誤是認為“搶佔式”只不過是“分時”的一種別稱而已)。搶佔式調度模型是指可能有多個線程是可運行的,但只有一個線程在實際運行。

這個線程會一直運行,直至它不再是可運行的,或者另一個具有更高優先順序的線程成為可運行的。對於後面一種情形,則是因低優先順序線程被高優先順序線程搶佔了運行的機會。

一個線程可能因為各種原因而不再是可運行的:線程的代碼可能執行了一個Thread.sleep()調用,要求這個線程暫停一段固定的時間;這個線程可能在等待訪問某個資源,而且在這個資源可訪問之前,這個線程無法繼續運行。圖10.4

所有可運行線程根據優先順序保存在池中。當一個被阻塞的線程變成可運行時,它會被放回相應的可運行池。優先順序最高的非空池中的線程會得到處理機時間(被運行)。因為Java線程不一定是分時的,所有你必須確保你的代碼中的線程會不時地給另外一個線程運行的機會。這可以通過在各種時間間隔中發出sleep()調用來做到。來看如下程式段:

publicclassTestimplementsRunnable

{ publicvoidrun(){while(true){ //dolotsofinterestingstuff//Giveotherthreadsachancetry{Thread.sleep(10);}catch(InterruptedExceptione){//Thisthread'ssleepwasinterrupted//byanotherthread} } }}

注意try和catch塊的使用。Thread.sleep()和其他使線程暫停一段時間的方法是可中斷的。線程可以調用另外一個線程的interrupt()方法,這將向暫停的線程發出一個InterruptedException。

Thread類的sleep()方法對當前線程操作,因此被稱作Thread.sleep(x),它是一個靜態方法。sleep()的參數指定以毫秒為單位的線程最小休眠時間,除非線程因為中斷而提早恢復執行,否則它不會在這段時間之前恢復執行。Thread類的另一個方法yield(),可以用來使具有相同優先順序的線程獲得執行的機會。如果具有相同優先順序的其他線程是可運行的,yield()將把調用線程放到可運行池中並使另一個線程運行。如果沒有相同優先順序的可運行進程,yield()什麼都不做。

sleep()調用會給較低優先順序線程一個運行的機會。yield()方法只會給相同優先順序線程一個執行的機會。10.2.2線程的調度到目前為止,我們已經學習了創建和管理線程的基本知識。你需要做的就是啟動一個線程,並讓它運行。你的應用程式也許希望等待一個線程執行完畢,也許打算發送一個資訊給線程,或者只打算讓線程在處理之前休眠一會兒。線程類提供了四種對線程進行操作的重要方法:sleep()、join()、wait()和notify()。sleep()方法是使線程停止一段時間的方法。在sleep時間間隔期滿後,線程不一定立即恢復執行。這是因為在那個時刻,其他線程可能正在運行而且沒有被調度為放棄執行,除非:

(a)“醒來”的線程具有更高的優先順序;

(b)正在運行的線程因為其他原因而阻塞。

如果一個應用程式需要執行很多時間,比如一個耗時很長的計算工作,你可以把該計算工作設計成線程。但是,假定還有另外一個線程需要計算結果,當計算結果出來後,如何讓那個線程知道計算結果呢?解決該問題的一個方法是讓第二個線程一直不停地檢查一些變數的狀態,直到這些變數的狀態發生改變。這樣的方式在Unix風格的伺服器中常常用到。Java提供了一個更加簡單的機制,即線程類中的join()方法。join()方法使得一個線程等待另外一個線程結束後再執行。例如,一個GUI(或者其他線程)使用join()方法等待一個子線程執行完畢:

CompleteCalcThreadt=newCompleteCalcThread(); t.start(); //做一會兒其他的事情//然後等待

t.join(); //使用計算結果……join()方法有三種格式:●voidjoin():等待線程執行完畢。●voidjoin(longtimeout):最多等待某段時間讓線程完成。●voidjoin(longmilliseconds,

intnanoseconds):最多等待某段時間(毫秒+納秒),讓線程完成。

線程APIisAlive()同join()相關聯時,是很有用的。一個線程在start(此時run()方法已經啟動)之後,在stop之前的某時刻處於isAlive狀態。對於編寫線程的程式員來說,還有其他兩個有用的方法,即wait()和notify()。使用這兩個API,我們可以精確地控制線程的執行過程。關於這兩個方法的使用,將在後面詳細解釋。10.2.3線程的優先順序線程可以設定優先順序,高優先順序的線程可以安排在低優先順序線程之前完成。一個應用程式可以通過使用線程中的setPriority(int)方法來設置線程的優先順序大小。對於多線程程式,每個線程的重要程度是不盡相同的,如多個線程在等待獲得CPU時間時,往往需要優先順序高的線程優先搶佔到CPU時間得以執行;又如多個線程交替執行時,優先順序決定了級別高的線程得到CPU的次數多一些且時間長一些。這樣,高優先順序的線程處理的任務效率就高一些。Java中,線程的優先順序從低到高以整數1~10表示,共分為10級。設置優先順序是通過調用線程對象的setPriority()方法來進行的。設置優先順序的語句為

Threadthreadone=newThread();

//用Thread類的子類創建線程

Threadthreadtwo=newThread();

threadone.setPriority(6);//設置threadone的優先順序為6

threadtwo.setPriority(3);//設置threadtwo的優先順序為3

threadone.start();threadtwo.start();//strat()方法啟動線程這樣,線程threadone將會優先於線程threadtwo執行,並將佔有更多的CPU時間。該例中,優先順序設置放在線程啟動前。也可以在啟動後進行優先順序設置,以滿足不同的優先順序需求。10.3線程組

通常,一個程式可能包含若干線程,如何來管理這些線程呢?把這些線程按功能分類是個不錯的辦法。Java語言提供了線程組,線程組可以讓你同時控制一組線程。實際上,線程組就是一種可以管理一組線程的類。

可以用構造方法ThreadGroup()來構造一個線程組,如下所示:

StringgrounName=…; ThreadGroupg=newThreadGroup(groupName);

ThreadGroup()方法的參數表示一個線程組,因此該串參數必須是惟一的。也可以用Thread類的構造方法往一個指定的線程組裏添加新的線程:

Threadt=newThread(g,threadName);

activeCount()方法用於檢測某個指定線程組是否有線程處於活動狀態:

if(g.activeCount()==0) {//線程g的所有線程都已停止}

要中斷一個線程組中的所有線程,可以調用ThreadGroup類的方法interrupt():

errupt();

線程組可以嵌套,即線程組可以擁有子線程組。缺省時,一個新創建的線程或線程組都屬於當前線程組所屬的線程組。線程組的常用方法如下:●ThreadGroup(Stringname):創建一個新線程組,它的父線程組是當前線程組。●ThreadGroup(ThreadGroupparent,Stringname):創建一個新線程組,其父線程組由parent參數指定。●intactiveCount():返回當前線程組活動線程的上限。●intenumerate(Thread[]list):得到當前線程組各個活動線程的地址。●ThreadGroupgetParent():得到當前線程組的父線程組。●Voidinterrupt():中斷線程組中所有線程及其子線程組中所有線程。10.4多線程程式的開發10.4.1synchronized的基本概念關鍵字synchronized提供Java編程語言一種機制,允許程式員控制共用數據的線程。本節重點討論其使用方法。我們已經知道,進程允許兩個或者更多個線程同時執行。實際上,這些線程也可以共用對象和數據,但在這種情形下,不同的線程在同一時間內不能存取同一數據,這是因為在開始設計Java的時候,就採用了線程的概念。Java語言定義了一個特殊的關鍵字synchronized(同步),該關鍵字可以應用到代碼塊上(代碼塊也包括入口方法)。該關鍵字的目的是防止多個線程在同一時間執行同一代碼塊內的代碼。定義一個同步方法的格式如下:

[public|private]synchronized{type}

methodname(...)

一個簡單的應用例子如下:

publicclasssomeClass

{ publicvoidaMethod(){ ... synchronized(this){ //Synchronizedcodeblock}...}}

同步化的關鍵字可以保證在同一時間內只有一個線程可以執行某代碼段,而任何其他要用到該段代碼的線程將被阻塞,直到第一個線程執行完該段代碼,如圖10.5所示對象鎖標誌synchronized到底是如何做到保證資源訪問同步的呢?在Java技術中,每個對象都有一個和它相關聯的標誌。這個標誌可以被認為是“鎖標誌”。synchronized關鍵字能保證多線程之間的同步運行,即允許獨佔地存取對象。當線程運行到synchronized語句時,它檢查作為參數傳遞的對象,並在繼續執行之前試圖從對象獲得鎖標誌。圖10.5

意識到它自身並沒有保護數據是很重要的。因為如果同一個對象的pop()方法沒有受到synchronized的影響,且pop()是由另一個線程調用的,那麼仍然存在破壞data的一致性的危險。如果要使鎖有效,所有存取共用數據的方法必須在同一把鎖上同步。圖10.6顯示了如果pop()受到synchronized的影響,且另一個線程在原線程持有那個對象的鎖時試圖執行pop()方法時所發生的事情:圖10.6

當線程試圖執行synchronized(this)語句時,它試圖從this對象獲取鎖標誌。由於得不到標誌,所以線程不能繼續運行。然後,線程加入到與那個對象鎖相關聯的等待線程池中。當標誌返回給對象時,某個等待這個標誌的線程將得到這把鎖並繼續運行。由於等待一個對象的鎖標誌的線程在得到標誌之前不能恢復運行,所以讓持有鎖標誌的線程在不再需要的時候返回標誌是很重要的。

鎖標誌將自動返回給它的對象。持有鎖標誌的線程執行到synchronized()代碼塊末尾時將釋放鎖。Java技術特別注意了保證即使出現中斷或異常而使得執行流跳出synchronized()代碼塊,鎖也會自動返回。此外,如果一個線程對同一個對象兩次發出synchronized調用,則在跳出最外層的塊時,標誌會正確地釋放,而最內層的將被忽略。這些規則使得與其他系統中的等價功能相比,管理同步塊的使用簡單了很多。10.4.2多線程的控制線程有兩個缺陷:死鎖和饑餓。所謂死鎖,就是一個或者多個線程,在一個給定的任務中,協同作用,互相干涉,從而導致一個或者更多線程永遠等待下去。與此類似,所謂饑餓,就是一個線程永久性地佔有資源,使得其他線程得不到該資源。

首先我們看一下死鎖的問題。一個簡單的例子就是:你到ATM機上取錢,卻看到如下的資訊“現在沒有現金,請等會兒再試。”,你需要錢,所以你就等了一會兒再試,但是你又看到了同樣的資訊;與此同時,在你後面,一輛運款車正等待著把錢放進ATM機中,但是運款車到不了ATM取款機,因為你的汽車擋著道。在這種情況下,就發生了所謂的死鎖。

在饑餓的情形下,系統並不處於死鎖狀態中,因為有一個進程仍在處理之中,只是其他進程永遠得不到執行的機會而已。在什麼樣的環境下,會導致饑餓的發生,並沒有預先確定好的規則。但一旦發生下麵四種情況之一,就會導致死鎖的發生。●相互排斥:一個線程或者進程永遠佔有一共享資源,例如,獨佔該資源。●迴圈等待:進程A等待進程B,而後者又在等待進程C,而進程C又在等待進程A。●部分分配:資源被部分分配。例如,進程A和B都需要訪問一個檔,並且都要用到印表機,進程A獲得了檔資源,進程B獲得了印表機資源。●缺少優先權:一個進程訪問了某個資源,但是一直不釋放該資源,即使該進程處於阻塞狀態。為了避免出現死鎖的情況,我們就必須在多線程程式中做同步管理,為此必須編寫使它們交互的程式。java.lang.Object類中提供了兩個用於線程通信的方法:wait()和notify()。如果線程對一個同步對象x發出一個wait()調用,則該線程會暫停執行,直到另一個線程對同一個同步對象x也發出一個wait()調用。為了讓線程對一個對象調用wait()或notify(),線程必須鎖定那個特定的對象。也就是說,只能在它們被調用的實例的同步塊內使用wait()和notify()。當某個線程執行包含對一個特定對象執行wait()調用的同步代碼時,這個線程就被放到與那個對象相關的等待池中。此外,調用wait()的線程自動釋放對象的鎖標誌。

對一個特定對象執行notify()調用時,將從對象的等待池中移走一個任意的線程,並放到鎖池中。鎖池中的對象一直在等待,直到可以獲得對象的鎖標記。notifyAll()方法將從等待池中移走所有等待那個對象的線程,並把它們放到鎖池中。只有鎖池中的線程能獲取對象的鎖標記,鎖標記允許線程從上次因調用wait()而中斷的地方開始繼續運行。

在許多實現了wait()/notify()機制的系統中,醒來的線程必定是那個等待時間最長的線程。然而,在Java技術中,並不能保證這點。應注意的是,不管是否有線程在等待,用戶都可以調用notify()。如果對一個對象調用notify()方法,而在這個對象的鎖標記等待池中並沒有阻塞的線程,那麼調用notify()將不起任何作用。對notify()的調用不會被存儲。

下麵給出一個線程交互的實例,用於說明如何使用synchronized關鍵字和wait()、notify()方法來解決線程的同步問題。例10.4DemoThread.javaimportjava.lang.Runnable;importjava.lang.Thread;

publicclassDemoThreadimplementsRunnable

{publicDemoThread() { TestThreadtestthread1=newTestThread(this,"1"); TestThreadtestthread2=newTestThread(this,"2"); testthread2.start(); testthread1.start(); }

publicstaticvoidmain(String[]args){

DemoThreaddemoThread1=newDemoThread(); }

publicvoidrun() { TestThreadt=(TestThread)Thread.currentThread(); try { if(!t.getName().equalsIgnoreCase("1")){

synchronized(this) { wait(); } }

while(true) {System.out.println("@timeinthread"+ t.getName()+"="+t.increaseTime()); if(t.getTime()%10==0) { synchronized(this) { System.out.println("********************************"); notify(); if(t.getTime()==100) break; wait(); } } } } catch(Exceptione) { e.printStackTrace(); } }}classTestThreadextendsThread{privateinttime=0;

publicTestThread(Runnabler,Stringname){ super(r,name);}

publicintgetTime(){ returntime;}

publicintincreaseTime(){ return++time;}}

本例實現了兩個線程,每個線程輸出1~100的數字。工作程式是:第一個線程輸出1~10,停止,通知第二個線程;第二個線程輸出1~10,停止,再通知第一個線程;第一個線程輸出11~20……

在Java中,每個對象都有個對象鎖標誌(Objectlockflag)與之想關聯,當一個線程A調用對象的一段synchronized代碼時,它首先要獲取與這個對象關聯的對象鎖標誌,然後才執行相應的代碼,執行結束後,把這個對象鎖標誌返回給對象。因此,線上程A執行synchronized代碼期間,如果另一個線程B也要執行同一對象的一段synchronized代碼(不一定與線程A執行的相同),那麼它必須等到線程A執行完後,才能繼續。

在synchronized代碼被執行期間,線程可以調用對象的wait()方法,釋放對象鎖標誌,進入等待狀態,並且可以調用notify()或者notifyAll()方法通知正在等待的其他線程。notify()方法通知等待佇列中的第一個線程,notifyAll()方法通知的是等待佇列中的所有線程。程式的部分輸出結果如下:

@timeinthread1=1@timeinthread1=2@timeinthread1=3@timeinthread1=4@timeinthread1=5@timeinthread1=6@timeinthread1=7@timeinthread1=8@timeinthread1=9@timeinthread1=10********************************@timeinthread2=1@timeinthread2=2@timeinthread2=3@timeinthread2=4@timeinthread2=5@timeinthread2=6@timeinthread2=7@timeinthread2=8@timeinthread2=9@timeinthread2=10********************************

線程中還有三個控制線程執行的方法:suspend()、resume()和stop()方法。但從JDK1.2開始,Java標準不贊成使用它們來控制線程的執行,而是用wait()和notify()來代替它們。這裏我們只對這三個方法做個簡單地介紹。●suspend()和resume()方法

resume()方法的惟一作用就是恢復被掛起的線程。所以,如果沒有suspend(),resume()也就沒有存在的必要。從設計的角度來看,有兩個原因使得使用suspend()非常危險:它容易產生死鎖;它允許一個線程式控制制另一個線程代碼的執行。下麵分別介紹這兩種危險。假設有兩個線程:threadA和threadB。當正在執行它們的代碼時,threadB獲得一個對象的鎖,然後繼續它的任務。當threadA的執行代碼調用threadB.suspend()時,將使threadB停止執行它的代碼。

如果threadB.suspend()沒有使threadB釋放它所持有的鎖,就會發生死鎖。如果調用threadB.resume()的線程需要threadB仍持有的鎖,這兩個線程就會陷入死鎖。假設threadA調用threadB.suspend()。如果threadB被掛起時threadA獲得控制,那麼threadB就永遠得不到機會來進行清除工作,例如使它正在操作的共用數據處於穩定狀態。為了安全起見,只有threadB才可以決定何時停止它自己的代碼。

你應該使用對同步對象調用wait()和notify()的機制來代替suspend()和resume()進行線程式控制制。這種方法是通過執行wait()調用來強制線程決定何時“掛起”自己。這使得同步對象的鎖被自動釋放,並給予線程一個在調用wait()之前穩定任何數據的機會。●stop()方法

stop()方法的情形是類似的,但結果有所不同。如果一個線程在持有一個對象鎖的時候被停止,它將在終止之前釋放它持有的鎖。這避免了前面所討論的死鎖問題,但它又引入了其他問題。

在前面的範例中,如果線程在已將字元加入棧但還沒有使下標值加1之後被停止,則在釋放鎖的時候會得到一個不一致的棧結構。總會有一些關鍵操作需要不可分割地執行,而且線上程執行這些操作時被停止就會破壞操作的不可分割性。

一個關於停止線程的獨立而又重要的問題涉及線程的總體設計策略。創建線程來執行某個特定作業,並存活於整個程式的生命週期。換言之,你不會這樣來設計程式:隨意地創建和處理線程,或創建無數個對話框或socket端點。每個線程都會消耗系統資源,而系統資源並不是無限的。這並不是暗示一個線程必須連續執行;它只是簡單地意味著應當使用合適而安全的wait()和notify()機制來控制線程。10.4.3多線程之間的通信

Java語言提供了各種各樣的輸入/輸出流,使我們能夠很方便地對數據進行操作。其中,管道(Pipe)流是一種特殊的流,用於在不同線程間直接傳送數據(一個線程發送數據到輸出管道,另一個線程從輸入管道中讀數據)。通過使用管道,就可實現不同線程間的通信,無需求助於類似臨時檔之類的東西。Java提供了兩個特殊的、專門的類用於處理管道,它們就是PipedInputStream類和PipedOutputStream類。PipedInputStream代表了數據在管道中的輸出端,也就是線程向管道讀數據的一端;PipedOutputStream代表了數據在管道中的輸入端,也就是線程向管道寫數據的一端,這兩個類一起使用可以提供數據的管道流。為了創建一個管道流,我們必須首先創建一個PipedOutStream對象,然後創建PipedInputStream對象,前面我們已經介紹過。

一旦創建了一個管道,就可以像操作檔一樣對管道進行數據的讀/寫。下麵我們來看一個運用管道實現線程間通信的實例。例10.5PipeTest.javaimportjava.util.*;importjava.io.*;

publicclassPipeTest

{

publicstaticvoidmain(Stringargs[]){

try{ /*setuppipes*/PipedOutputStreampout1=newPipedOutputStream();PipedInputStreampin1=newPipedInputStream(pout1)

PipedOutputStreampout2=newPipedOutputStream();Pi

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论