java编程思想多线程部分Word文档格式.docx

上传人:b****1 文档编号:15315156 上传时间:2022-10-29 格式:DOCX 页数:24 大小:46.77KB
下载 相关 举报
java编程思想多线程部分Word文档格式.docx_第1页
第1页 / 共24页
java编程思想多线程部分Word文档格式.docx_第2页
第2页 / 共24页
java编程思想多线程部分Word文档格式.docx_第3页
第3页 / 共24页
java编程思想多线程部分Word文档格式.docx_第4页
第4页 / 共24页
java编程思想多线程部分Word文档格式.docx_第5页
第5页 / 共24页
点击查看更多>>
下载资源
资源描述

java编程思想多线程部分Word文档格式.docx

《java编程思想多线程部分Word文档格式.docx》由会员分享,可在线阅读,更多相关《java编程思想多线程部分Word文档格式.docx(24页珍藏版)》请在冰豆网上搜索。

java编程思想多线程部分Word文档格式.docx

“线程”是进程内部单一的一个顺序控制流。

因此,一个进程可能容纳了多个同时执行的线程。

多线程的应用范围很广。

但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行。

这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并让它独立于主程序运行。

一个很好的例子便是“Quit”或“退出”按钮——我们并不希望在程序的每一部分代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。

事实上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。

14.1反应灵敏的用户界面

作为我们的起点,请思考一个需要执行某些CPU密集型计算的程序。

由于CPU“全心全意”为那些计算服务,所以对用户的输入十分迟钝,几乎没有什么反应。

在这里,我们用一个合成的applet/application(程序片/应用程序)来简单显示出一个计数器的结果:

752-753页程序

在这个程序中,AWT和程序片代码都应是大家熟悉的,第13章对此已有很详细的交待。

go()方法正是程序全心全意服务的对待:

将当前的count(计数)值置入TextField(文本字段)t,然后使count增值。

go()内的部分无限循环是调用sleep()。

sleep()必须同一个Thread(线程)对象关联到一起,而且似乎每个应用程序都有部分线程同它关联(事实上,Java本身就是建立在线程基础上的,肯定有一些线程会伴随我们写的应用一起运行)。

所以无论我们是否明确使用了线程,都可利用Thread.currentThread()产生由程序使用的当前线程,然后为那个线程调用sleep()。

注意,Thread.currentThread()是Thread类的一个静态方法。

注意sleep()可能“掷”出一个InterruptException(中断违例)——尽管产生这样的违例被认为是中止线程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。

再次提醒大家,违例是为异常情况而产生的,而不是为了正常的控制流。

在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。

一旦按下start按钮,就会调用go()。

研究一下go(),你可能会很自然地(就象我一样)认为它该支持多线程,因为它会进入“睡眠”状态。

也就是说,尽管方法本身“睡着”了,CPU仍然应该忙于监视其他按钮“按下”事件。

但有一个问题,那就是go()是永远不会返回的,因为它被设计成一个无限循环。

这意味着actionPerformed()根本不会返回。

由于在第一个按键以后便陷入actionPerformed()中,所以程序不能再对其他任何事件进行控制(如果想出来,必须以某种方式“杀死”进程——最简便的方式就是在控制台窗口按Ctrl+C键)。

这里最基本的问题是go()需要继续执行自己的操作,而与此同时,它也需要返回,以便actionPerformed()能够完成,而且用户界面也能继续响应用户的操作。

但对象go()这样的传统方法来说,它却不能在继续的同时将控制权返回给程序的其他部分。

这听起来似乎是一件不可能做到的事情,就象CPU必须同时位于两个地方一样,但线程可以解决一切。

“线程模型”(以及Java中的编程支持)是一种程序编写规范,可在单独一个程序里实现几个操作的同时进行。

根据这一机制,CPU可为每个线程都分配自己的一部分时间。

每个线程都“感觉”自己好象拥有整个CPU,但CPU的计算时间实际却是在所有线程间分摊的。

线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得了巨大的利益。

综合考虑,这一机制是非常有价值的。

当然,如果本来就安装了多块CPU,那么操作系统能够自行决定为不同的CPU分配哪些线程,程序的总体运行速度也会变得更快(所有这些都要求操作系统以及应用程序的支持)。

多线程和多任务是充分发挥多处理机系统能力的一种最有效的方式。

14.1.1从线程继承

为创建一个线程,最简单的方法就是从Thread类继承。

这个类包含了创建和运行线程所需的一切东西。

Thread最重要的方法是run()。

但为了使用run(),必须对其进行过载或者覆盖,使其能充分按自己的吩咐行事。

因此,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。

下面这个例子可创建任意数量的线程,并通过为每个线程分配一个独一无二的编号(由一个静态变量产生),从而对不同的线程进行跟踪。

Thread的run()方法在这里得到了覆盖,每通过一次循环,计数就减1——计数为0时则完成循环(此时一旦返回run(),线程就中止运行)。

755页程序

run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。

因此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从run()返回即可)。

run()通常采用一种无限循环的形式。

也就是说,通过阻止外部发出对线程的stop()或者destroy()调用,它会永远运行下去(直到程序完成)。

在main()中,可看到创建并运行了大量线程。

Thread包含了一个特殊的方法,叫作start(),它的作用是对线程进行特殊的初始化,然后调用run()。

所以整个步骤包括:

调用构建器来构建对象,然后用start()配置线程,再调用run()。

如果不调用start()——如果适当的话,可在构建器那样做——线程便永远不会启动。

下面是该程序某一次运行的输出(注意每次运行都会不同):

756页程序

可注意到这个例子中到处都调用了sleep(),然而输出结果指出每个线程都获得了属于自己的那一部分CPU执行时间。

从中可以看出,尽管sleep()依赖一个线程的存在来执行,但却与允许或禁止线程无关。

它只不过是另一个不同的方法而已。

亦可看出线程并不是按它们创建时的顺序运行的。

事实上,CPU处理一个现有线程集的顺序是不确定的——除非我们亲自介入,并用Thread的setPriority()方法调整它们的优先级。

main()创建Thread对象时,它并未捕获任何一个对象的句柄。

普通对象对于垃圾收集来说是一种“公平竞赛”,但线程却并非如此。

每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。

这样一来,垃圾收集器便只好对它“瞠目以对”了。

14.1.2针对用户界面的多线程

现在,我们也许能用一个线程解决在Counter1.java中出现的问题。

采用的一个技巧便是在一个线程的run()方法中放置“子任务”——亦即位于go()内的循环。

一旦用户按下Start按钮,线程就会启动,但马上结束线程的创建。

这样一来,尽管线程仍在运行,但程序的主要工作却能得以继续(等候并响应用户界面的事件)。

下面是具体的代码:

757-759页程序

现在,Counter2变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。

但假若用户现在按下Start按钮,却不会真正调用一个方法。

此时不是创建类的一个线程,而是创建SeparateSubTask,然后继续Counter2事件循环。

注意此时会保存SeparateSubTask的句柄,以便我们按下onOff按钮的时候,能正常地切换位于SeparateSubTask内部的runFlag(运行标志)。

随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将SeparateSubTask设为一个内部类来达到这一目的)。

SeparateSubTask类是对Thread的一个简单扩展,它带有一个构建器(其中保存了Counter2句柄,然后通过调用start()来运行线程)以及一个run()——本质上包含了Counter1.java的go()内的代码。

由于SeparateSubTask知道自己容纳了指向一个Counter2的句柄,所以能够在需要的时候介入,并访问Counter2的TestField(文本字段)。

按下onOff按钮,几乎立即能得到正确的响应。

当然,这个响应其实并不是“立即”发生的,它毕竟和那种由“中断”驱动的系统不同。

只有线程拥有CPU的执行时间,并注意到标记已发生改变,计数器才会停止。

1.用内部类改善代码

下面说说题外话,请大家注意一下SeparateSubTask和Counter2类之间发生的结合行为。

SeparateSubTask同Counter2“亲密”地结合到了一起——它必须持有指向自己“父”Counter2对象的一个句柄,以便自己能回调和操纵它。

但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到Java确实提供了合并它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。

但不管怎样,它们依然紧密地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。

在这种情况下,一个内部类可以显著改善代码的“可读性”和执行效率:

759-761页程序

这个SeparateSubTask名字不会与前例中的SeparateSubTask冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。

大家亦可看到内部类被设为private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为public,因为它在基础类中是公开的)。

除Counter2i之外,其他任何方面都不可访问private内部类。

而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。

在SeparateSubTask中,我们可看到invertFlag()方法已被删去,因为Counter2i现在可以直接访问runFlag。

此外,注意SeparateSubTask的构建器已得到了简化——它现在唯一的用外就是启动线程。

Counter2i对象的句柄仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。

在run()中,可看到对t的访问是直接进行的,似乎它是SeparateSubTask的一个字段。

父类中的t字段现在可以变成private,因为SeparateSubTask能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。

无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。

14.1.3用主类合并线程

在上面的例子中,我们看到线程类(Thread)与程序的主类(Main)是分隔开的。

这样做非常合理,而且易于理解。

然而,还有另一种方式也是经常要用到的。

尽管它不十分明确,但一般都要更简洁一些(这也解释了它为什么十分流行)。

通过将主程序类变成一个线程,这种形式可将主程序类与线程类合并到一起。

由于对一个GUI程序来说,主程序类必须从Frame或Applet继承,所以必须用一个接口加入额外的功能。

这个接口叫作Runnable,其中包含了与Thread一致的基本方法。

事实上,Thread也实现了Runnable,它只指出有一个run()方法。

对合并后的程序/线程来说,它的用法不是十分明确。

当我们启动程序时,会创建一个Runna

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > PPT模板 > 图表模板

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1