关于现代程序的编写一般都是基于多处理或者多内核处理器来进行的,所以多线程并发处理设计成为提高应用程序运行效率的首选,我在前面的几篇文章中都详细介绍了Java语言对于多线程并发编程设计的一些主要内容和思想,本文将从一个整体的角度来串联一下,有关程序,进程,多任务设计以及并发编程等概念,对Java多线程编程来个总结性说明。
程序(program),进程(process),多任务(multitasking),顺序编程(Sequential Programming),并发编程(concrrent programming)。
我们知道程序就是使用某种特定的编程语言将一个算法表示出来。
那么进程呢,就是这段程序在操作系统上会被读取并分配了其所定义的所有系统资源而开始运行的一个实例。
通常一个进程在操作系统上运行必须有一个唯一标识符(PID),一个程序计数器(PC),包括可执行的代码片段,还有可供使用的内存地址空间,以及能够调用操作系统资源的句柄,当然这些之外还需要一个安全不受干扰的上下文环境等等。
程序计数器又被称为指令指针,它是在CPU的寄存器里维护的一个值,用来追踪CPU执行的指令地址。CPU执行完一条指令它的值会自动增加。
我们还可以将进程理解为操作系统中一个活动单元,或者一个工作单元,一个执行单元,或者一个执行路径。
进程概念的定义能够让一个计算机系统支持多个执行单元运行。
多任务:是指操作系统能够一次执行多个任务(即多个进程)的能力。
由于CPU每次只能执行一条指令,所以在单个CPU机器上真正意义上的多任务是不能实现的。
在这种情况下,操作系统通过将单个CPU的时间分配给所有正在运行的进程,并在进程之间进行足够快的切换来实现多任务处理,从而给人一种所有进程都在同时运行的印象。
进程之间的CPU切换称为上下文切换。
在上下文切换中,正在运行的进程被停止,它的状态被保存,将要获得CPU的进程的状态被恢复,新的进程被运行。
有必要在将CPU分配给另一个进程之前保存正在运行的进程的状态,这样当这个进程再次获得CPU时,它就可以从离开的地方开始执行。
通常,进程的状态由程序计数器、进程使用的寄存器值和其他以后恢复进程所需的任何信息组成。
操作系统存储进程状态的数据结构,被称为进程控制块(PCB)。
但要注意,线程的上下文切换是相当昂贵的操作,尽量减少强制这类切换。
多任务处理有两种类型:协同式和抢占式。
在协同多任务处理中,正在运行的进程决定何时释放CPU,以便其他进程可以使用CPU。
在抢占式多任务处理中,操作系统为每个进程分配一个时间片。一旦一个进程用完它的时间片,它就会被抢占,操作系统就会把CPU分配给另一个进程。
在协同多任务处理中,一个进程可能长时间独占CPU,其他进程可能没有机会运行。
在抢占式多任务处理中,操作系统确保所有进程都能获取CPU时间。
UNIX,OS/2和Windows使用抢占式的多任务处理,其中Windows 3.x使用协同式多任务处理。
多任务处理是指计算机同时使用多个处理器的能力。
并行处理是系统在多个处理器上同时执行同一任务的能力。
对于并行处理,必须将任务分解为多个子任务,以便可以在多个处理器上同时执行这些子任务。
假设我们设计一个包含六条指令的程序:
Instruction-1
Instruction-2
Instruction-3
Instruction-4
Instruction-5
Instruction-6
为了完整地执行这个程序,CPU必须执行所有六条指令,假设前三条指令相互依赖,即假设Instruction-2使用Instruction-1的结果,Instruction-3使用Instruction-2的结果。
假设后三条指令也像前三条指令一样相互依赖。而前三条指令和后三条指令作为两个组,彼此不依赖。
如果我们希望执行这六个指令以获得最佳的结果该如何执行呢?
当然,最直接能想到的其中一种方法是按顺序执行它们,遵照它们出现在程序中的顺序被执行。
这样执行的话,会为我们的程序提供一个执行序列,可以看成一个进程。
当然还可以有另一种执行方法,就是按两个序列来执行。
比如执行一个序列Instruction-1, Instruction-2和 Instruction-3,同时,另一个执行序列会执行Instruction-4,Instruction-5, Instruction-6指令组。
其实我们可以看到,这两组执行序列的“执行单位”和“执行顺序”的是相同的,完全可以互换进行。
这里要注意了,由于进程也是一个执行单元。所以,这两组指令可以作为两个进程来运行,如此我们就可以以在执行过程中实现并发性。
到目前为止,请注意我们假设这两组指令是相互独立的,执行过程中是相互无干扰的。
这时我们要问了,如果这两组指令访问一个共享内存会怎样?
或者,当这两组指令完成运行时,我们需要合并这两组指令的结果来计算最终结果?
首先要注意,由于进程在运行时通常不允许访问另一个进程的地址空间。
进程间的通信必须必须使用诸如socket、管道等进程间通信设施进行交互。
当多个进程需要通信或共享资源时,进程的本质是一个独立于其他进程运行的代码片段,所以可能会造成问题。
所有现代操作系统都允许我们在一个进程中创建多个执行单元来解决进程间无法通信这个问题,其中所有执行单元都可以共享分配给该进程的地址空间和资源。
进程中的每个执行单元称为线程,即CPU每次可执行处理的单位。
我们知道,我们编写一段代码来解决一个问题,其实就是设计一段程序,让其在操作系统上运行为一个进程,该进程至少有一个线程,这就是我们的主线程。Java编程里的Main函数启用该进程。当然如果需要通过复杂步骤来解决问题,在可以为该进程创建多个线程。
通常一个进程可以创建的最大线程数由操作系统及其可用的资源数决定。前面我在多篇文章中专门对这个问题进行了讨论,这里就不再赘述了。
一个进程中的所有线程共享该线程管理的所有资源,包括分配给该进程的内存地址空间,同一进程中的所有线程可以很容易地相互通信,因为它们在相同的进程中执行操作,并且共享相同的内存。
由于一个进程中的每个线程独立于同一进程中的其他线程运行,所以每个线程会独自维护两样东西:
程序计数器和堆栈。
程序计数器让线程跟踪它当前正在执行的指令。
因为进程中的每个线程可能同时执行不同的指令,所以必须为每个线程维护单独的程序计数器。
每个线程都维护自己的堆栈来存储本地变量的值。
一个线程还可以维护它的私有内存,即使这些内存在同一个进程中它们也不能与其他线程共享私有内存。
线程维护的私有内存称为线程本地存储(TLS)。
其实现在所有的操作系统中,线程都是CPU调度执行的单位,而不是进程。也就是说CPU调度的不是进程,而是以线程为操作执行单位的。
所以,我们可以讲CPU的上下文切换都是发生在线程之间的。
当然,进程也存在着在CPU上的运行切换,比如我们给某个程序以焦点,或者激活它。
与进程之间的上下文切换相比,线程之间的上下文切换成本更低。
由于易于在进程内的线程之间进行通信、共享资源以及更便宜的上下文切换,所以最好将程序拆分为多个线程,而不是多个进程。
有时线程也被称为轻量级进程。如前所述,带有六条指令的程序也可以在一个进程中分成两个线程。
在多处理器机器上,一个进程的多个线程可能被调度在不同的处理器上,从而提供了一个程序的真正的并发执行。
所以我们可以将进程和线程之间的关系视为:
进程 = 地址空间 + 计算资源 + 线程
线程是进程内的执行单元,它们维护自己独特的程序计数器和堆栈或者叫私有内存空间,这些线程共享进程的地址空间和所拥有管理的资源。
每个线程都可以被单独安排在一个可用的CPU上执行。
由于现在Java的高并发应用程序编写,需要对应用程序,进程,线程,并发和并行应用程序等概念有一个准确的理解,才能在程序设计实现过程中,准确的把握线程的应用,以及线程之间通信方式的选择,才能更好的理解JDK提供的并发线程池的设计理念和用途。