购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

第二卷 第七章 多线程

7.1多线程的概念

多线程编程的含义是你可将程序任务分成几个并行的子任务。特别是在网络编程中,你会发现很多功能是可以并发执行的。比如网络传输速度较慢,用户输入速度较慢,你可以用两个独立的线程去完成这?copy;功能,而不影响正常的显示或其他功能。多线程是与单线程比较而言的,普通的WINDOWS采用单线程程序结构,其工作原理是:主程序有一个消息循环,不断从消息队列中读入消息来决定下一步所要干的事情,一般是一个子函数,只有等这个子函数执行完返回后,主程序才能接收另外的消息来执行。比如子函数功能是在读一个网络数据,或读一个文件,只有等读完这?copy;数据或文件才能接收下一个消息。在执行这个子函数过程中你什么也不能干。但往往读网络数据和等待用户输入有很多时间处于等待状态,多线程利用这个特点将任务分成多个并发任务后,就可以解决这个问题。

7.1.1Java线程的模型

Java的设计思想是建立在当前大多数操作系统都实现了线程调度。Java虚拟机的很多任务都依赖线程调度,而且所有的类库都是为多线程设计的。实时上,Java支持Macintosh和Ms-dos的平台?reg;所以迟迟未出来就是因为这两个平台都不支持多线程。Java利用多线程实现了整个执行环境是异步的。在Java程序里没有主消息循环。如果一个线程等待读取网络数据,它可以运行但不停止系统的其他线程执行。用于处理用户输入的线程大多时间是等待用户敲键盘或击鼠标。你还可以使动画的每一帧?reg;间停顿一秒而并不使系统暂停。一?copy;线程启动后,它可以被挂起,暂时不让它执行。挂起的线程可以重新恢复执行。任何时间线程都可以被停止,被停止的线程就不能再重新启动。Java语言里,线程表现为线程类,线程类封装了所有需要的线程操作控制。在你心里,必须很清晰地区分开线程对象和运行线程,你可以将线程对象看作是运行线程的控制面板。在线程对象里有很多函数来控制一个线程是否运行,睡眠,挂起或停止。线程类是控制线程行为的唯一的手段。一?copy;一个Java程序启动后,就已经有一个线程在运行。你可通过调用Thread.currentThread函数来查看当前运行的是哪一个线程。

你得到一个线程的控制柄,你就可以作很有趣的事情,即使单线程也一样。下面这个例子让你知道怎样操纵当前线程。Filename:testthread

classtestthread{publicstaticvoidmain(Stringargs){Threadt

=Thread.currentThread;t.setName(\"ThisThreadisrunning\");

System.out.println(\"Therunningthread:\"+t);try{for(inti=0;i<5;i++)

{System.out.println(\"Sleeptime\"+i);Thread.sleep(1000);}

}catch(InterruptedExceptione){System.out.println(\"threadhaswrong\");}

}}

执行结果:javatestthreadTherunningthread:Thread[ThisThreadisrunning,5,main]Sleeptime0Sleeptime1Sleeptime2Sleeptime3Sleeptime4

7.1.2启动接口

一个线程并不激动人心,多个线程才有实际意义。我们怎样创建更多的线程呢?我们需要创建线程类的另一个实例。当我们构造了线程类的一个新的实例,我们必须告诉它在新的线程里应执行哪一段程序。你可以在任意实现了启动接口的对象上启动一个线程。启动接口是一个抽象接口,来表示本对象有一?copy;函数想异步执行。要实现启动接口,一个类只需要有一个叫run的函数。下面是创建一个新线程的例子:

Filename:twothread.java

classtwothreadimplementsRunnable{twothread{Threadt1

=Thread.currentThread;t1.setName(\"Thefirstmainthread\");

System.out.println(\"Therunningthread:\"+t1);Threadt2=new

Thread(this,\"thesecondthread\");System.out.println(\"creatanother

thread\");t2.start;try{System.out.println(\"firstthreadwill

sleep\");Thread.sleep(3000);}catch(InterruptedExceptione)

{System.out.println(\"firstthreadhaswrong\");}

System.out.println(\"firstthreadexit\");}publicvoidrun{try{for

(inti=0;i<5;i++){System.out.println(\"Sleeptimeforthread2:\"+i);

Thread.sleep(1000);}

}catch(InterruptedExceptione){System.out.println(\"threadhas

wrong\");}

System.out.println(\"secondthreadexit\");}publicstaticvoid

main(Stringargs){newtwothread;}}

执行结果:javatwothread

Therunningthread:Thread[Thefirstmainthread,5,main]creatanother

threadfirstthreadwillsleepSleeptimeforthread2:0Sleeptimefor

thread2:1Sleeptimeforthread2:2firstthreadexitSleeptimefor

thread2:3Sleeptimeforthread2:4secondthreadexit

main线程用newThread(this,\"thesecondthread\")创建了一个Thread对象,通过传递第一个参数来标明新线程来调用this对象的run函数。然后我们调用start函数,它将使线程从run函数开始执行。

7.1.3同步

因为多线程给你提?copy;了程序的异步执行的功能,所以在必要时必须还提?copy;一种同步机制。例如,你想两个线程通讯并共享一个复杂的数据结构,你需要一种机制让他们相互牵制并正确执行。为这个目的,Java用一种叫监视器(monitor)的机制实现了进程间的异步执行。可以将监视器看作是一个很小的盒子,它只能容纳一个线程。一?copy;一个线程进入一个监视器,所有其他线程必须等到第一个线程退出监视器后才能进入。这?copy;监视器可以设计成保护共享的数据不被多个线程同时操作。大多数多线程系统将这?copy;监视器设计成对象,Java提?copy;了一种更清晰的解决方案。没有Monitor类;每个对象通过将他们的成员函数定义成synchronized来定义自己的显式监视器,一?copy;一个线程执行在一个synchronized函数里,其他任何线程都不能调用同一个对象的

synchronized函数。

7.1.4消息

你的程序被分成几个逻辑线程,你必须清晰的知道这?copy;线程?reg;间应怎样相互通讯。Java提了wait和notify等功能来使线程?reg;间相互交谈。一个线程可以进入某一个对象的synchronized函数进入等待状态,直到其他线程显式地将它唤醒。可以有多个线程进入同一个函数并等待同一个唤醒消息。

7.2Java线程例子

7.2.1显式定义线程

在我们的单线程应用程序里,我们并没有看见线程,因为Java能自动创建和控制你的线程。如果你使用了理解Java语言的浏览器,你就已经看到使用多线程的Java程序了。你也许注意到两个小程序可以同时运行,或在你移动滚动条时小程序继续执行。这并不是表明小程序是多线程的,但说明这个浏览器是多线程的。多线程应用程序(或applet)可以使用好几个执行上下文来完成它们的工

作。多线程利用了很多任务包含单独的可分离的子任务的特点。每一个线程完成一个子任务。但是,每一个线程完成子任务时还是顺序执行的。一个多线程程序允许各个线程尽快执行完它们。这种特点会有更好的实时输入反应。

7.2.2多线程例子

下面这个例子创建了三个单独的线程,它们分别打印自己的\"HelloWorld\":

//Defineoursimplethreads.Theywillpauseforashorttime//andthen

printouttheirnamesanddelaytimesclassTestThreadextendsThread

{privateStringwhoami;privateintdelay;

//Ourconstructortostorethename(whoami)//andtimetosleep(delay)

publicTestThread(Strings,intd){whoami=s;delay=d;}

//Run-thethreadmethodsimilartomain//Whenrunisfinished,the

threaddies.//RuniscalledfromthestartmethodofThreadpublicvoid

run{//Trytosleepforthespecifiedtimetry{sleep(delay);}

catch(InterruptedExceptione){}//Nowprintoutourname

System.out.println(\"HelloWorld!\"+whoami+\"\"+delay);}}/***Multimtest.

Asimplemultithreadthestprogram*/publicclassmultitest{public

staticvoidmain(Stringargs){TestThreadt1,t2,t3;//Createourtest

threadst1=newTestThread(\"Thread1\",(int)(Math.readom*2000));t2=

newTestThread(\"Thread2\",(int)(Math.readom*2000));t3=new

TestThread(\"Thread3\",(int)(Math.readom*2000));

//Starteachofthethreadst1.start;t2.start;t3.start;}}

7.2.3启动一个线程

程序启动时总是调用main函数,因此main是我们创建和启动线程的地方:

t1=newTestThread(\"Thread1\",(int)(Math.readom*2000));

这一行创建了一个新的线程。后面的两个参数传递了线程的名称和线程在打印信息?reg;前的延时时间。因为我们直接控制线程,我们必须直接启动它:t1.start;

7.2.4操作线程

如果创建线程正常,t1应包含一个有效的执行线程。我们在线程的run函数里控制线程。一?copy;我们进入run函数,我们便可执行里面的任何程序。run好象main一样。

run执行完,这个线程也就结束了。在这个例子里,我们试着延迟一个随机的时间(通过参数传递?)sleep(delay);

sleep函数只是简单地告诉线程休息多少个毫秒时间。

如果你想推迟一个线程的执行,你应使用sleep函数。当线程睡眠是sleep并不占用系统资源。其它线程可继续工作。一?copy;延迟时间完毕,它将打印\"HelloWorld\"和线程名称及延迟时间。

7.2.5暂停一个线程

我们经常需要挂起一个线程而不指定多少时间。例如,如果你创建了一个含有动画线程的小程序。也许你让用户暂停动画至到他们想恢复为止。你并不想将动画线程仍调,但想让它停止。象这种类似的线程你可用suspend函数来控制:t1.suspend;这个函数并不永久地停止了线程,你还可用resume函数重新激活线程:t1.resume;

7.2.6停止一个线程

线程的最后一个控制是停止函数stop。我们用它来停止线程的执行:t1.stop;

注意:这并没有消灭这个线程,但它停止了线程的执行。并且这个线程不能用t1.start重新启动。在我们的例子里,我们从来不用显式地停止一个线程。我们只简单地让它执行完而已。很多复杂的线程例子将需要我们控制每一个线程。在这种情况下会使用到stop函数。如果需要,你可以测试你的线程是否被激活。一个线程已经启动而且没有停止被认为是激活的。t1.isAlive如果t1是激活的,这个函数将返回true.

7.2.7动画例子

下面是一个包含动画线程的applet例子:

importjava.awt.*;importjava.awt.image.ImageProducer;import

java.applet.Applet;

publicclassatest3extendsAppletimplementsRunnable{Imageimages;

MediaTrackertracker;intindex=0;Threadanimator;

intmaxWidth,maxHeight;//Ouroff-screencomponentsfordoublebuffering.

ImageoffScrImage;GraphicsoffScrGC;

//Canwepaintyes?booleanloaded=false;

//Initializetheapplet.Setoursizeandloadtheimagespublicvoidinit

[//Setupourimagemonitortracker=newMediaTracker(this);

//SetthesizeandwidthofourappletmaxWidth=100;maxHeight=100;

images=newImage[10];//Setupthedouble-bufferandresizeourapplet

try{offScrImage=createImage(maxWidth,maxHeight);offScrGC=

offScrImage.getGraphics;offScrGC.setColor(Color.lightGray);

offScrGC.fillRect(0,0,maxWidth,maxHeight);

resize(maxWidth,maxHeight);}catch(Exceptione)

{e.printStackTrace;}

//loadtheanimationimagesintoanarrayfor(inti=0;i<10;i++){String

imageFile=newString(\"images/Duke/T\"+String.valueOf(i+1)+\".gif\");

images[i]=getImage(getDocumentBase,imageFile)://Registerthis

imagewiththetrackertracker.addImage(images[i],i);}try{//Use

trackertomakesurealltheimagesareloadedtracker.waitForAll;}

catch(InterruptedExceptione){}loaded=true;}

//Paintthecurrentframe.publicvoidpaint(Graphicsg){if(loaded)

{g.drawImage(offScrImage,0,0,this);}}

//Start,setupourfirstimagepublicvoidstart{if(tracker.checkID

(index)){offScrGC.drawImage(images[index],0,0,this);}animator=new

Thread(this);animator.start;}

//Run,dotheanimationworkhere.//Grabanimage,pause,grabthenext...

publicvoidrun{//GettheidofthecurrentthreadThreadme=

Thread.currentThread;

//Ifouranimatorthreadexist,andisthecurrentthread...while

((animatr!=null)&&(animator==me)){if(tracker.checkID(index))

{//Clearthebackgroundandgetthenextimage

offScrGC.fillRect(0,0,100,100);

offScrGCdrawImage(images[index],0,0,this);index++;//Loopbacktothe

beginningandkeepgoingif(index>=images.length){index=0;}}

//Delayheresoanimationlooksnormaltry{animator.sleep(200);}catch

(InterruptedExceptione){}//Drawthenextframerepaint;}}}

7.3多线程间的通讯

7.3.1生产者和消费者

多线程的一个重要特点是它们?reg;间可以互相通讯。你可以设计线程使用公用对象,每个线程都可以独立操作公用对象。典型的线程间通讯建立在生产者和消费者模型上:一个线程产生输出;另一个线程使用输入buffer

让我们创建一个简单的\"AlphabetSoup\"生产者和相应的消费者.

7.3.2生产者

生产者将从thread类里派生:classProducerextendsThread

{privateSoupsoup;privateStringalphabet=\"

ABCDEFGHIJKLMNOPQRSTUVWXYZ\";

publicProducer(Soups){//Keepourowncopyofthesharedobjectsoup

=s;}

publicvoidrun{charc;//Throw10lettersintothesoupfor(int

i=0;i<10;i++){c=alphabet.charAt((int)(Math.random*26));

soup.add(c);//printarecordofosraddition

System.out.println(\"Added\"+c+\"tothesoup.\");//waitabitbeforewe

addthenextlettertry{sleep((int)(Math.random*1000));}catch

(InterruptedExceptione){}}}}

注意我们创建了Soup类的一个实例。生产者用soup.add函数来建立字符池。

7.3.3消费者

让我们看看消费者的程序:classConsumerextendsThread{privateSoupsoup;

publicConsumer(Soups){//keepourowncopyofthesharedobjectsoup

=s;}

publicvoidrun{charc;//Eat10lettersfromthealphabetsoupfor

(intI=0;i<10;i++){//graboneletterc=soup.eat;//Printoutthe

letterthatweretrievedSystem.out.println(\"Atealetter:\"+c);//try

{sleep((int)(Math.raddom*2000));}catch(InterruptedExceptione){}}}}

同理,象生产者一样,我们用soup.eat来处理信息。那么,Soup类到底干什么呢?

7.3.4监视

Soup类执行监视两个线程?reg;间传输信息的功能。监视是多线程中不可缺少的一部分,因为它保持了通讯的流?copy;。让我们看看Soup.java文件:classSoup{privatecharbuffer=newchar[6];privateintnext=0;//Flagstokeeptrackof

ourbufferstatusprivatebooleanisFull=false;privatebooleanisEmpty

=true;publicsyschronizedchareat{//Wecan\'teatifthereisn\'tanything

inthebufferwhile(isEmpty==true){try{wait;//we\'llexitthis

whenisEmptyturnsfalse}catch(InterruptedExceptione){}}//decrement

thecount,sincewe\'regoingtoeatoneletternext--;//Didweeatthe

lastletter?if(next==0){isEmpty=true;}//Weknowthebuffercan\'t

befull,becausewejustateisFull=false;notify;//returntheletter

tothethreadthatiseatingreturn(buffer[next]);}

//methodtoaddletterstothebufferpublicsynchronizedvoidadd(char

c){//Waitarounduntilthere\'sroomtoaddanotherletterwhile(isFull

==true){try{wait;//ThiswillexitwhenisFullturnsfalse}catch

(InterruptedExceptione){}}//addthelettertothenextavailablespot

buffer[next]=c;//Changethenextavailablespotnext++;//Arewefull;

if(next==6){isFull=true;}isEmpty=false;notify;}}soup类包含两个重要特征:数据成员buffer是私有的,功能成员add和eat是公有的。

数据私有避免了生产者和消费者直接获得数据。直接访问数据可能造成错误。例如,如果消费者企图从空缓冲区里取出数据,你将得到不必要的异常,否则,你只能锁住进程。同步访问方法避免了破坏一个共享对象。当生产者向soup里加入一个字母时,消费者不能吃字符,诸如此类。这种同步是维持共享对象完整性的重要方面。notify函数将唤醒每一个等待线程。等待线程将继续它的访问。

7.3.5联系起来

现在我们有一个生产者,一个消费者和一个共享对象,怎样实现它们的交互呢?我们只需要一个简单的控制程序来启动所有的线程并确信每一个线程都是访问的同一个共享对象。下面是控制程序的代码,SoupTest.java:

classSoupTest{publicstaticvoidmain(Stringargs){Soups=new

Soup;Producerp1=newProducer(s);Consumerc1=newConsumer(s);

p1.start;c1.start;}}

7.3.6监视生产者

生产者/消费者模型程序经常用来实现远程监视功能,它让消费者看到生产者同用户的交互或同系统其它部分的交互。例如,在网络中,一组生产者线程可以在很多工作站上运行。生产者可以打印文档,文档打印后,一个标志将保存下来。一个(或多个?copy;消费者将保存标志并在晚上报告白天打印活动的情况。另外,还有例子在一个工作站是分出几个独立的窗口。一个窗口用作用户输入(生产者)另一个窗口作出对输入的反应(消费者)。

7.4线程API列表

下面是一个常用的线程类的方法函数列表:类函数:以下是Thread的静态函数,即可以直接从Thread类调用。

currentThread返回正在运行的Thread对象yield停止运行当前线程,让系统运行下一个线程sleep(intn)让当前线程睡眠n毫秒对象函数:以下函数必须用Thread的实例对象来调用。

startstart函数告诉java运行系统为本线程建立一个执行环境,然后调用本线程的run函数。run是运行本线程的将要执行的代码,也是Runnable接口的唯一函数。当一个线程初始化后,由start函数来调用它,一?copy;run函数返回,本线程也就终止了。stop让某线程马上终止,系统将删除本线程的执行环境suspend与stop函数不同,suspend将线程暂停执行,但系统不破坏线程的执行环境,你可以用resume来恢复本线程的执行resume恢复被挂起的线程进入运行状态setPriority(intp)给线程设置优先级getPriority返回线程的优先级setName(Stringname)给线程设置名称getName取线程的名称

本章小结:

1.多线程是java语言的重要特点,java语言用Thread类封装了线程的所有操作。2.线程的接口名为Runnable3.线程间同步机制为synchronized关键词4.线程间通讯靠wait与notify消息 E6uCj2SOYhOF9vi3WfyI/6acBrs6oUDvIS3NLET4g7gBPg0uus4965UCzVy8r2Cf

点击中间区域
呼出菜单
上一章
目录
下一章
×