友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第92部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
Vector 的其他所有行为。由于 elementAt()需要大量进行“封闭”工作,用到许多括号,所以随着代码主体
的扩充,最终仍有可能需要大量代码。
和以前一样,在我们实现Runnable 的时候,并没有获得与 Thread 配套提供的所有功能,所以必须创建一个
新的Thread,并将自己传递给它的构建器,以便正式“启动”——start()——一些东西。大家在
CBoxVector 构建器和 go()里都可以体会到这一点。run()方法简单地选择Vector 里的一个随机元素编号,并
为那个元素调用nextColor(),令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下
来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的
考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试
着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问
题,那么现在有许多因素需要检查:
(1) 对 sleep,yield()以及/或者wait()的调用足够多吗?
(2) sleep()的调用时间足够长吗?
(3) 运行的线程数是不是太多?
(4) 试过不同的平台和 JVM 吗?
象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。
534
…………………………………………………………Page 536……………………………………………………………
14。6 总结
何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进
行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资
源均衡的经典问题是在 IO等候期间如何利用 CPU。至于用户方面的方便性,最经典的问题就是如何在一个长
时间的下载过程中监视并灵敏地反应一个“停止”(stop )按钮的按下。
多线程的主要缺点包括:
(1) 等候使用共享资源时造成程序的运行速度变慢。
(2) 对线程进行管理要求的额外CPU 开销。
(3) 复杂程度无意义的加大,比如用独立的线程来更新数组内每个元素的愚蠢主意。
(4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。
线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000 条指
令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变
量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。
线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一
系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了
对线程的扩展。不管在什么情况下,涉及线程的程序设计:
(1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路;
(2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。
尽管对线程的支持使Java 语言的复杂程度多少有些增加,但请不要责怪 Java 。毕竟,利用线程可以做许多
有益的事情。
多个线程可能共享同一个资源(比如一个对象里的内存),这是运用线程时面临的最大的一个麻烦。必须保
证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用 synchronized (同步)关键字。它是一
个有用的工具,但必须真正掌握它,因为假若操作不当,极易出现死锁。
除此以外,运用线程时还要注意一个非常特殊的问题。由于根据Java 的设计,它允许我们根据需要创建任意
数量的线程——至少理论上如此(例如,假设为一项工程方面的有限元素分析创建数以百万的线程,这对
Java 来说并非实际)。然而,我们一般都要控制自己创建的线程数量的上限。因为在某些情况下,大量线程
会将场面变得一团糟,所以工作都会几乎陷于停顿。临界点并不象对象那样可以达到几千个,而是在 100 以
下。一般情况下,我们只创建少数几个关键线程,用它们解决某个特定的问题。这时数量的限制问题不大。
但在较常规的一些设计中,这一限制确实会使我们感到束手束脚。
大家要注意线程处理中一个不是十分直观的问题。由于采用了线程“调度”机制,所以通过在run()的主循
环中插入对 sleep()的调用,一般都可以使自己的程序运行得更快一些。这使它对编程技巧的要求非常高,
特别是在更长的延迟似乎反而能提高性能的时候。当然,之所以会出现这种情况,是由于在正在运行的线程
准备进入“休眠”状态之前,较短的延迟可能造成“sleep()结束”调度机制的中断。这便强迫调度机制将其
中止,并于稍后重新启动,以便它能做完自己的事情,再进入休眠状态。必须多想一想,才能意识到事情真
正的麻烦程度。
本章遗漏的一件事情是一个动画例子,这是目前程序片最流行的一种应用。然而,Java JDK 配套提供了解决
这个问题的一整套方案(并可播放声音),大家可到java。sun。 的演示区域下载。此外,我们完全有理由
相信未来版本的Java 会提供更好的动画支持——尽管目前的 Web 涌现出了与传统方式完全不同的非 Java、
非程序化的许多动画方案。如果想系统学习Java 动画的工作原理,可参考《Core Java——核心Java 》一
书,由Corn ell&Horstmann 编著,Prentice …Hall 于 1997 年出版。若欲更深入地了解线程处理,请参考
《Concurrent Programming in Java——Java 中的并发编程》,由Doug Lea 编著,Addison…Wiseley 于
1997 年出版;或者《Java Threads——Java 线程》,Oaks&Wong 编著,O'Reilly 于 1997 年出版。
14。7 练习
(1) 从Thread 继承一个类,并(过载)覆盖 run()方法。在run()内,打印出一条消息,然后调用
sleep()。重复三遍这些操作,然后从run()返回。在构建器中放置一条启动消息,并覆盖 finalize(),打印
一条关闭消息。创建一个独立的线程类,使它在run()内调用System。gc()和 System。runFinalization(),
并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。
(2) 修改Counter2。java ,使线程成为一个内部类,而且不需要明确保存指向Counter2 的一个。
(3) 修改Sharing2。java,在TwoCounter 的run()方法内部添加一个synchronized (同步)块,而不是同步
整个run()方法。
535
…………………………………………………………Page 537……………………………………………………………
(4) 创建两个Thread 子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread 对象的句柄,然
后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一
条消息。
(5) 在Ticker2 内的Counter5。java 中,删除yield(),并解释一下结果。用一个sleep()换掉yield(),再
解释一下结果。
(6) 在ThreadGroup1。java 中,将对sys。suspend()的调用换成对线程组的一个wait()调用,令其等候2 秒
钟。为了保证获得正确的结果,必须在一个同步块内取得 sys 的对象锁。
(7) 修改Daemons。java,使main()有一个 sleep(),而不是一个readLine()。实验不同的睡眠时间,看看会
有什么发生。
(8) 到第7 章(中间部分)找到那个GreenhouseControls。java 例子,它应该由三个文件构成。在
Event。java 中,Event 类建立在对时间的监视基础上。修改这个 Event,使其成为一个线程。然后修改其余
的设计,使它们能与新的、以线程为基础的Event 正常协作。
536
…………………………………………………………Page 538……………………………………………………………
第 15 章 网络编程
历史上的网络编程都倾向于困难、复杂,而且极易出错。
程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议
中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、
打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。
但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者
相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者
发送的数据。
Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏
在JVM 以及Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接
(一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,
在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便
的。
本章将用一系列易懂的例子解释Java 的连网支持。
15。1 机器的标识
当然,为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一
无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但 Java
面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了 IP
(互联网地址)的概念。IP 以两种形式存在着:
(1) 大家最熟悉的DNS (域名服务)形式。我自己的域名是bruceeckel。。所以假定我在自己的域内有一
台名为Opus 的计算机,它的域名就可以是 Opus。bruceeckel。。这正是大家向其他人发送电子函件时采用
的名字,而且通常集成到一个万维网(WWW)地址里。
(2) 此外,亦可采用“四点”格式,亦即由点号(。)分隔的四组数字,比如202。98。32。111 。
不管哪种情况,IP地址在内部都表达成一个由32 个二进制位(bit)构成的数字(注释①),所以IP地址
的每一组数字都不能超过255。利用由java 提供的 static InetAddress。getByName(),我们可以让一个
特定的 Java 对象表达上述任何一种形式的数字。结果是类型为InetAddress 的一个对象,可用它构成一个
“套接字”(Socket),大家在后面会见到这一点。
①:这意味着最多只能得到40 亿左右的数字组合,全世界的人很快就会把它用光。但根据目前正在研究的新
IP编址方案,它将采用 128 bit 的数字,这样得到的唯一性IP地址也许在几百年的时间里都不会用完。
作为运用 InetAddress。getByName()一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者
(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时 IP地址。但在连接期间,那
个 IP 地址拥有与因特网上其他 IP 地址一样的有效性。如果有人按照你的 IP地址连接你的机器,他们就有可
能使用在你机器上运行的Web 或者 FTP 服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的
IP。由于每次拨号连接获得的IP 都是随机的,怎样才能准确地掌握你的IP 呢?
下面这个程序利用 InetAddress。getByName()来产生你的 IP 地址。为了让它运行起来,事先必须知道计算机
的名字。该程序只在Windows 95 中进行了测试,但大家可以依次进入自己的 “开始”、“设置”、“控制面
板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。
//: WhoAmI。java
// Finds out your network address when you're
// connected to the Internet。
package c15;
import java。*;
public class WhoAmI {
public static void main(String'' args)
537
…………………………………………………………Page 539……………………………………………………………
throws Exception {
if(args。length != 1) {
System。err。println(
〃Usage: WhoAmI MachineName〃);
System。exit(1);
}
InetAddress a =
InetAddress。getByName(args'0');
System。out。println(a);
}
} ///:~
就我自己的情况来说,机器的名字叫作“Colossus”(来自同名电影,“巨人”的意思。我在这台机器上有
一个很大的硬盘)。所以一旦连通我的ISP,就象下面这样执行程序:
java whoAmI Colossus
得到的结果象下面这个样子(当然,这个地址可能每次都是不同的):
Colossus/202。98。41。151
假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人Web 服务器,只需指定目标地址
http://202。98。41。151 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的
Web 站点正式出台以前进行测试的一种方便手段。
15。1。1 服务器和客户机
网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对
方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台
机器不得不停留在一个地方,侦听其他机器说:“嘿,你在哪里呢?”
“停留在一个地方”的机器叫作“服务器”(Server);到处“找人”的机器则叫作“客户机”(Client)
或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成
了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。
所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务
是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务
器端还是客户机端,连接只是魔术般地变成了一个 IO数据流对象。从这时开始,我们可以象读写一个普通的
文件那样对待连接。所以一旦建好连接,我们只需象第 10章那样使用自己熟悉的 IO 命令即可。这正是 Java
连网最方便的一个地方。
1。 在没有网络的前提下测试程序
由于多种潜在的原因,我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是
在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。IP 的设计者注
意到了这个问题,并建立了一个特殊的地址——localhost——来满足非网络环境中的测试要求。在 Java 中
产生这个地址最一般的做法是:
InetAddress addr = InetAddress。getByName(null);
如果向 getByName()传递一个null (空)值,就默认为使用localhost。我们用InetAddress 对特定的机器
进行索引,而且必须在进行进一步的操作之前得到这个 InetAddress (互联网地址)。我们不可以操纵一个
InetAddress 的内容(但可把它打印出来,就象下一个例子要演示的那样)。创建 InetAddress 的唯一途径
就是那个类的static (静态)成员方法getByName() (这是最常用的)、getAllByName()或者
getLocalHost()。
为得到本地主机地址,亦可向其直接传递字串〃localhost〃:
InetAddress。getByName(〃localhost〃);
或者使用它的保留 IP 地址(四点形式),就象下面这样:
InetAddress。getByName(〃127。0。0。1〃);
这三种方法得到的结果是一样的。
538
…………………………………………………………Page 540……………………………………………………………
15。1。2 端口:机器内独一无二的场所
有些时候,一个 IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服
务器(程序)。由 IP 表达的每台机器也包含了“端口”(Port )。我们设置一个客户机或者服务器的时候,
必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,IP 地址是他居住的房子,
而端口是他在的那个房间。
注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知
道如何通过机器的 IP 地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着
一种服务,一台机器可能提供了多种服务,比如HTTP 和 FTP 等等)?端口编号在这里扮演了重要的角色,它
是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服
务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口
编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。
系统服务保留了使用端口 1 到端口 1024 的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在
使用的端口。本书的第一个例子将使用端口8080 (为追忆我的第一台机器使用的老式8 位 Intel 8080芯
片,那是一部使用CP/M 操作系统的机子)。
15。2 套接字
“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。
针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的
每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知
的。抽象的基本宗旨是让我们尽可能不必知道那些细节。
在Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个 InputStream 以
及OutputStream (若使用恰当的转换器,则分别是Reader 和 Writer),以便将连接作为一个IO流对象对
待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用
它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方
法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”
连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用 getInputStream()以及
getOutputStream()从每个套接字产生对应的 InputStream 和OutputStream 对象。这些数据流必须封装到缓
冲区内。可按第 10章介绍的方法对类进行格式化,就象对待其他任何流对象那样。
对于Java 库的命名机制,ServerSocket
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!