编写正确的程序并不容易,而编写正确的并发程序就更难了。与顺序执行的程序相比,并发程序中显然更容易出现错误。那么,我们为什么会对并发如此烦恼呢?线程是Java语言不可避免的特性,它们把复杂、异步的代码转化为更简单、更直观的代码,从而简化复杂系统的开发。进一步而言,线程是控制和利用多处理器系统计算能力的最简单方式。同时,伴随着处理器数量的增加,有效地采用并发会变得越来越重要。
1.1 并发的(非常)简短历史
在发展的初期,计算机还没有操作系统;它们自始至终执行一个程序,这个程序直接访问机器的所有资源。这样一个程序运行在无保护的金属器件上,不仅写起来困难,而且每次只运行一个程序,不能很好地利用昂贵且稀缺的计算机资源。
操作系统的发展使得多个程序能够同时运行,程序在各自的进程(processes)中运行:相互分离,各自独立执行,由操作系统来分配资源,比如内存、文件句柄、安全证书。如果需要的话,进程会通过一些原始的机制相互通信:Socket、信号处理(signal handlers)、共享内存(shared memory)、信号量(semaphores)和文件。
有一些促进因素,它们推动了操作系统支持多程序同时执行的发展:
资源利用。程序有时候需要等待外部的操作,比如输入和输出,并且在等待的时候不可能进行有价值的工作。在等待的时候,让其他的程序运行会提高效率。
公平。多个用户或程序可能对系统资源有平等的优先级别。让他们通过更好的时间片方式来共享计算机,这要比结束一个程序后才开始下一个程序更可取。
方便。写一些程序,让它们各自执行一个单独任务并进行必要的相互协调,这要比编写一个程序来执行所有的任务更容易,更让人满意。
在早期的分时共享系统中,每一个进程都是一个虚拟的冯诺依曼(von Neumann)机;它拥有一个内存空间,储存着指令和数据,根据机器语言的语义来顺序地执行指令,并且通过操作系统的I/O原语(primitive)集来实现与外部世界的交互。对于每一条指令的执行,都有一个对“下一条指令”的明确定义,并根据程序中的指令集来进行流程的控制。现在几乎所有广泛使用的编程语言都遵循这个顺序的编程模型,其中语言规范明确定义了在一个给定动作完成后,下一个动作是什么。
顺序编程模型是自然的、常规的,就像是遵守着人类的工作方式:一次做一件事情,顺序进行——通常如此。起床,穿上浴衣,下楼,开始准备早茶。在编程语言中,真实世界中的每一个动作,都会抽象成一个规则的动作序列——打开食品柜,选择你喜欢的茶,在罐里放入适量的茶叶,看看茶壶中是不是有足够多的水,如果不够就加些水,把茶壶放在炉子上,打开炉子,等待水的沸腾等等。最后一步——等待水沸腾——也引入了异步这个要点。当水在加热的时候,你可以选择做什么——等待,或者开始准备吐丝面包(另一个异步任务),还可以取一份报纸看,同时仍然要记得煮开水的壶马上就会需要你的关注。开水和吐丝面包的生产者知道他们的产品通常在异步的情况下使用,所以在任务结束的时候,它们会提高信号的音量。找到顺序和异步之间最好的平衡,通常是那些高效率人士的一个特点——对于程序来说也是如此。
相同的关注点(资源利用,公平和方便)不仅促进了进程的发展,也促进了线程的发展。线程允许程序控制流(control flow)的多重分支同时存在于一个进程。它们共享进程范围内的资源,比如内存和文件句柄,但是每一个线程有其自己的程序计数器(program counter)、栈(stack)和本地变量。线程也为多处理器系统中并行地使用硬件提供了一个自然而然的分解;同一程序内的多个线程可以在多CPU的情况下同时调度。
线程有些时候被称为轻量级进程(lightweight processes),并且大多数现代操作系统把线程作为时序调度的基本单元,而不是进程。在没有明确协调的情况下,线程相互间同时或异步地执行。因为线程共享其所属进程的内存地址空间,因此所有同一进程中的线程访问相同的变量,并从同一个堆中分配对象,这相对于进程间通信(inter-process)机制来说实现了良好的数据共享。但是如果没有明确的同步来管理共享数据,一个线程可能会修改其他线程正在使用的数据,产生意外的结果。






