首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

除少数专用系统和单用户单任务操作系统之外,大多数操作系统都允许创建多个进程。当一个程序启动时,它可以为即将开始的每项任务创建一个进程,并允许它们同时运行。当一个程序因等待网络访问或用户输入而被阻塞时,另一个程序还可以运行,这样就提高了资源利用率。但是,创建每个进程都要付出一定的代价,设置一个进程也要占用一部分处理器时间和内存资源。而且,操作系统不允许进程访问其他进程的内存空间。因此,进程间的通信很不方便,并且在可见的将来也不会围绕进程出现更高效的编程模型。

线程可以被认为是“轻型进程”。线程只能在单个进程的作用域内活动,所以创建线程比创建进程要廉价得多。现代操作系统普遍以线程为单位来分配CPU时间片,多线程应用程序不仅能提高系统资源利用率,而且在很多场合能极大地改善用户的使用体验。

多线程编程是一项相当充满诱惑力的技能,它更像一把双刃剑:使用得当的话能使应用程序的整体性能迈上更高层次,但是稍有不慎,就会跌入陷阱。在单线程编程中,程序员仅需关注代码的顺次执行的逻辑,只要测试用例覆盖面足够,就能保证逻辑正确;而在多线程编程中,有些错误是由于并发引起的,除非“机缘巧合”,否则不能再现。

无论如何,多线程编程都是现代程序开发人员不能回避的课题,现在就让我们进入Java多线程开发的殿堂。

本章主要内容包括:

¿ 线程简介

¿ 多线程应用开发

¿ 线程间通信

¿ 实战多线程下载

本章首先介绍线程理论,然后介绍多线程编程的基础技能:创建和启动线程、管理线程的状态、线程组,接着介绍如何利用管道在线程之间实现传输信息。最后利用本章介绍的多线程技术开发一个类似网络蚂蚁的多线程下载程序。

7.1  线程简介

7.1.1  什么是线程

现代操作系统在开始运行一个程序时,至少会创建一个进程(Process)。每个进程至少有一个运行中的线程(Thread)。线程是通过程序执行代码的一条路径,每个线程均有自己的局部变量、程序计数器(当前执行指令的指针)以及生存期。大部分现代操作系统允许在一个进程内并发运行多个线程。

举例来说,当操作系统启动Java虚拟机时,就创建了一个进程,在该进程内可以创建很多线程。人们通常认为Java程序是从main()方法开始执行,并通过程序内的路线向前推进,直到完成了main()中所有的语句为止。这描述的是单线程的情形,也就是主线程。主线程由Java虚拟机创建,它通过main()方法开始执行,一直执行完main()中的所有语句,完成main()方法后消亡。不为人所知的第二个线程总是在Java虚拟机内部运行,即垃圾回收线程。垃圾回收线程清除被废弃的对象,并回收它们占用的内存。因此,即使是一个只完成打印“Hello, world”任务的Java程序也是运行在一个多线程的环境中,这两个线程便是主线程和垃圾回收线程。

而如果执行中的Java程序是一个图形界面(GUI)程序,Java虚拟机会自动启动更多的线程。其中一个线程负责给程序中的方法传送GUI事件,另一个线程负责绘制GUI窗体。

例如,在一个耗时较长的交互过程中,当任务提交给后台执行之后,在用户界面上往往需要显示一个进度条,显示进度条的任务就是由另一个线程完成的。再例如,如果用户在任务执行的中途单击“取消”按钮中断任务的执行,那么GUI事件线程调用为“取消”按钮编写的点击事件响应代码,终止执行任务的线程。总之,在GUI程序中,如果仅有一个线程,是无法既显示界面,又执行用户提交的任务,而且在任务的执行过程中响应各种事件的。

7.1.2  多线程的用途

在某一些场合,引入多线程机制有利于提升程序的运行效果;而在另一些场合,必须引入多线程机制才能满足需求。毋庸讳言,多线程编程比起单线程编程,其难度和出错的风险绝对不是简单的根据线程数翻倍的关系。

在单线程编程中,程序员仅仅需要关注代码的顺次执行的逻辑,只要测试用例覆盖面足够,就能保证逻辑正确(注意,也仅仅是保证逻辑正确,至于程序运行的异常风险,则需要开展更广泛的测试)。

而在多线程编程中,有些错误是由于并发引起的,而这些错误能否发生,往往还取决于其他线程当时的运行状态。因为子线程和主线程之间异步执行的关系,即使发现过一次错误,要再现错误也是不容易的。

但是在现代的计算机编程中,多线程开发是不容回避的课题,否则就永远只能徘徊在低水平上。下面列举多线程机制存在的理由。

1.与用户的更佳交互

在改善用户的使用体验方面的例子很多。

例如,我们在Windows桌面上复制一个大文件,Windows会循环放映一个纸片从一个文件夹飞到另一个文件夹的动画,放映动画和复制文件就是分处两个线程中。试想如果没有这段带有提示意味的动画“陪伴”我们打发寂寞时光,我们就连是否复制完毕都不知道。

再例如,我们在Word字处理软件中一边输入文字,Word会适时地一边帮助我们进行拼写检查。拼写检查就是在后台运行的一个线程,拼写检查线程无疑是要占用系统资源的,但是我们为什么不会感觉到文字输入过程的停顿呢?这是因为即使面对输入速度极快的人,计算机处理器的运算速度也是遥遥领先,完全有能力在用户敲击键盘停顿的瞬间将系统资源交给拼写检查线程。

2.模拟同步动作

早期的计算机操作系统都是基于任务(相当于今天的进程)来分配处理器运算资源的,而最终用户往往期望多任务同步并行,基于任务切换来模拟同步动作无疑不是最佳的途径。现代操作系统引入线程概念之后,任务被划分成了更细粒度的线程,基于线程切换来模拟同步动作,最终用户获得了多个任务并行处理的使用体验。

3.利用多处理器

谈到多任务同步处理,自然就不得不提到当前逐渐成为主流的多处理器(多核)计算机。在单处理器计算机上,采用线程切换的手段模拟同步动作,是否意味着在多处理器计算机上,就无须引入线程概念,便自然可以实现同步动作的效果了呢?

这里有两个误区:(1)处理器的数量永远跟不上用户期待同时进行的任务的数量;(2)人类永远不会觉得自己的计算机太快了,这是一条被反复印证的道理。人类社会越来越依赖计算机,这就决定了计算机要面临的计算任务只会越来越复杂:在图形界面出现之前,人们不会认为几百MB的外存储空间会不够使用,在多媒体时代之前,人们不会奢望在计算机上播放影音文件。

4.在等待阻塞性动作期间完成其他的任务

我们来思考一个关于网络服务器工作原理的问题。假设存在一个文件上传服务器,当一个用户上传一个较大的文件时,另一个用户同时上传另一个文件。如果文件上传服务器的开发者只懂得单线程编程的话,我们可以想象出后果:后面那个用户的上传请求将一直搁置,直到等待超时。这是因为在服务器端,接受客户端连接请求和读取客户端字节流的过程一旦运行在同一个线程中,那么就意味着它们组成了一组阻塞性动作,除非读取完毕,否则上传服务器是不能再接入新的客户端的。

对于网络服务器来说,适合的做法是把接受连接请求和读取客户端输入字节流分为两个线程,这样接受连接请求的线程可以在接收到一个连接请求之后立刻将该请求转交给另一个处理请求的线程,而自身继续侦听是否有新的连接请求。主流的网络服务器,如Apache,采用的就是这种工作模式。

至于在收到客户端连接请求之后,是新建一个处理客户端请求的线程,还是转交给一个早已创建好的处理线程以节省临时创建线程的时间消耗,这是多线程开发方面很需要讲究策略的一个问题,我们在本书的下一章将回答这个问题。

7.1.3  Java语言与多线程

目前已经有相当多的操作系统具有多线程处理能力,但是几乎每种支持多线程的操作系统,彼此之间在线程机制的运用和线程的处理方面存在相当的差异。如此一来,即使是用公认的可移植性极佳的C语言来编写多线程程序,也会受制于使用了某个操作系统中的多线程功能,而降低了C语言的可移植性。

如果一方面要求应用程序启用多线程机制,一方面又强调应用程序的可移植性的话,摆在开发人员面前的将是一个非常棘手的难题。好在对多线程机制的支持已经是现代操作系统的一个基本特征,因此对于占据后发优势的语言来说,在语言一级内置对多线程的支持就是最明智之举了,也就是将运用和处理多线程的功能,规范在语言本身的结构之中——Java语言便是这样做的,我们即将领略Java语言在多线程开发方面为我们准备好的一切。除Java语言之外,新生的C#也内置了对多线程的支持。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论