Java多线程笔记(零):进程、线程与通用概念

前言

不积跬步,无以致千里;不积小流,无以成江海。在学习Java多线程相关的知识前,咱们首先须要去了解一点操做系统的进程、线程以及相关的基础概念。数据结构

进程

一般,咱们把一个程序的执行称为一个进程。反过来说,进程用于描述程序的执行过程。所以,程序和进程是一对概念,它们分別描述了一个程序的静态和动态特征:除此以外,进程还操做系统进行资源分配的一个基本单位。多线程

进程的衍生

进程使用fork系统调用来建立。父进程调用fork建立子进程。每一个子进程都是源自它的父进程的一个副本,它会得到父进程的数据段、堆和栈的副本,并与父进程共享代码段。每一份副本都是独立的,子进程对属于它的副本的修改对其父进程和兄弟进程(同父进程)都是不可见的,反之亦然。全盘复制父进程的数据是一种至关低效的作法。 Linux操做系统内核使用写时复制(Copy on Write,常简称为COW)等技术来提升进程建立的效率。固然,刚建立的子进程也能够经过系统调用exec把一个新的程序加载到己的内存中,而原先在内存中的数据段、堆、栈以及代码段就会被替换掉,在这以后,子进程执行的就会是那个刚刚加载进来的新程序。并发

父进程被若是优先于子进程结束,那么子进程就会被原来父进程的父进程“收养”。异步

为了管理进程,内核必须对每一个进程的数据和行为进行详细的记录,包括进程的优先级、状态、虚拟地址范围以及各类访问权限等等。更具体地说,这些信息都会被记在每一个进程的进程描述符中。进程描述符并非一个简单的符号,而是一个很是复杂的数据结构。保存在进程描述符中的进程ID (常称为PID )是进程在操做系统中的惟一标识,其中进程ID为1的进程就是以前提到的内核启动进程。进程id是一个非负整数且老是顺序的编号,新建立的进程ID老是前一个进程ID递增的结果。此外,进程ID也能够重复使用。当进程ID达到其最大限值时,内核会从头开始查找闲置的进程ID并使用M先找到的那一个做为新进程的ID。另外,进程描述符中还会包含当前进程的父进程的ID (常称为PPID )。函数

进程间的同步

若是多个进程之间须要协做完成任务,那么进程间通讯的方式就是须要重点考虑的事项之一。这种通讯叫作IPC(Inter-Process Communication)。那么在Linux中,从处理机制的角度看,能够分为三大类方法:工具

  1. 基于通讯的IPC
  2. 基于信号的IPC
  3. 基于同步的IPC

通讯IPC

  • 以数据为传送手段的IPC学习

    • 管道(pipe):用于传输字节流
    • 消息队列(message queue):用来传输结构化的对象
  • 以共享内存为手段的IPCatom

    • 共享内存区(share memory):最快的IPC方法

信号IPC

  • 操做系统的信号(signal)机制:惟一一种异步IPC方法。经过kill -l查看。

同步IPC

  • 信号量(semaphore)

进程的状态

在Linux中,每一个进程在每一个时刻只会有一种状态,分别有如下六种spa

可运行状态(TASK_RUNNING)

该进程马上或正在CPU上运行。可是运行的时期是不肯定的,由进程调度来决定。操作系统

可中断的睡眠状态(TASK_INTERRUPTABLE)

若是一个进程正在等待某个事件到来时,会进入此状态。这样的进程会被放入对应的等待队列中。当事件发生时,对应的等待队列中的一个或多个进程就会被唤醒。

不可中断的睡眠状态(TASK_UNINTERRUPTIBLE)

此种状态可与中断的睡眠状态的惟一区别是它不可被打断。这意味着此种状态的进程不会对任何信号做出响应。更确切地讲,发送给此状态的进程的信号直到它状态转出才会被传递过去。处于此状态的进程一般是在等待一个特殊的时间,好比等待同步的IO操做完成。

暂停状态(TASK_STOPPED或TASK_TRACED)或跟踪状态

向进程发送SIGSTOP信号,就会使该进程转入暂停状态,除非该进程正处于不可中断的睡眠状态。

向正处于暂停的进程发送SIGCONT信号,会使用该进程转向可运行状态。处于该状态的进程会暂停,并等待另外一个进程(跟踪它的那个进程)对它进行操做。例如,咱们使用调试工具GDB在某个程序中设置一个断点,然后对应的进程运行到该断点处就会停下来。这时,该进程就处于跟踪状态。跟踪状态与暂停状态很是相似。可是,向处于跟踪状态的进程发送SIGCONT信号并不能使它回复。只有当调试进程进行了相应的系统调用或退出后,它才可以恢复。

僵尸状态(TASK_DEAD-EXIT_ZOMBIE)

处于此状态的进程即将结束运行,该进程占用的绝大多数资源也都已经被回收,不过还有一些信息未仍是拿出,好比退出码以及一些统计信息。之因此保留这些信息,主要是考虑到该进程的父进程可能须要它们。因为此时的进程主体已经被删除而只留下一个空壳,故此状态才被称为僵尸状态。

退出状态(TASK_DEAD-EXIT_DEAD)

在进程退出的过程当中,有可能连退出码和统计信息都不须要保留。形成这种状况的缘由多是显示地让该进程的父进程忽略掉SIGCHLD信号(当一个进程消亡的时候,内核会给其父进程发送SIGCHLD信号以告知此状况),也多是该进程已经被分离(分离即让子进程和父进程分别独立运行)。分离后的子程序将不会再使用和执行与父进程共享代码段中的指令,而是加载并运行一个全新的程序。在这些状况下,该进程处于退出的时候就不会转入僵尸状态,而会直接转入退出状态。处于退出状态的进程会当即被干净利落地结束掉,它占用的系统资源也会被操做系统自动回收。

内核为每一个用户进程分配的是虚拟内存而不是物理内存。同时,内核会把进程的虚拟内存划分为若干页(page),而物理内存单元的划分由CPU负责。一个物理内存单元被称为一个页框(page freame)。不一样进程的大多数页都会与不一样的页框相对应。对应的时候那就是共享内存了。

线程

线程能够视为进程中的控制流。一个进程至少包含一个线程,由于其余至少会有一个控制流持续运行。于是,一个进程的第一个线程会随着这个进程的启动而建立,这个线程被称为该进程的主线程。固然,一个进程能够包含多个线程。这些线程都是由当前线程中已经存在的线程建立出来的,建立的方法就是调用系统调用(pthread_create)。拥有多个线程的进程能够并发执行多个任务,而且即时某个或某些任务被阻塞,也不会影响其余任务执行,这能够大大改善程序的响应时间和吞吐量。另外一方面,线程不可能独立于进程存在。它的生命周期不可能逾越所属进程的生命周期。

一个进程中的全部线程都拥有本身线程栈,并以此存储本身的私有数据。这些线程的线程栈都包含在其所属进程的虚拟内存地址中。不过要注意,一个进程中的不少资源都会被其中的全部线程共享,这些被线程共享的资源包含当前进程所持有文件描述符,等等。正由于如此,同一个进程的多个线程运行的必定是同一个程序,只不过具体的控制流程的执行函数可能有所不一样。在同一个进程的多个线程之间共享数据也是一件很是轻松和天然的事情。另外,建立一个新线程,也不会像建立一个新进程那样耗时费力,由于在其所属进程的虚拟内存地址中存储的代码、数据和资源都不须要被复制。

另外,操做系统和提供了必定的系统调用用于管理当前进程中的线程。

线程的标识

和进程同样,每一个线程都有本身的ID(由内核分配),叫作线程ID或者TID。可是在操做系统范围内不惟一,在所属进程的范围内惟一。

线程的控制

任何一个线程均可以同一线程中的其余线程进行有限管理,以下:

建立线程

主线程在其所属进程启动时建立。其余线程能够经过别的线程用pthread_create来建立——要传入新线程将要执行的函数以及传入该函数的参数值。在建立成功的时候,该函数会返回线程的TID。

终止线程

线程能够经过多种方式来终结同一个进程中的其余线程。其余一种方式就是调用系统调用pthread_cancel,其做用是取消掉给定线程ID表明的那个线程。更确切地讲,它会向目标线程发送一个请求,要求它马上终止执行。可是该函数只是发送请求并便可返回。可是,该函数只是发送请求并马上返回,而不会等待目标线程对该请求作出响应。至于目标线程何时对此作出线程、怎么样的响应,则取决与另外的因素(好比线程目标的取消状态及类型)。在默认状况下,目标线程老是会接受线程取消请求,不过等到时机成熟(执行到某个取消点)的时候,目标线程才会响应线程的取消请求。

链接已终止的线程

此操做由系统调用pthread_join来执行,该函数会一直等待与给定的线程ID对应的那个线程终止,并把线程执行的pthread_create函数的返回值告知调用线程。若是目标线程已经处于终止状态,那么该函数会当即返回。这就像是把调用线程放置在了目标线程的后面,当目标线程把线程控制权交出时,调用线程会接过流程控制权并继续执行pthread_join函数调用以后的代码。这也把这一操做称为链接的原因之一。实际上,若是一个线程可被链接,那么在它终止以前就必须链接,不然就会变成一个僵尸线程。僵尸线程不但会致使系统资源浪费,还会无心义减小其进程的可建立线程数量。

分离线程

将一个线程分离后那么它将变得不可链接。而在默认状况下,一个线程老是能够被链接的。分离操做的另外一个做用是让操做系统内核在目标线程终止时自行进行清理和销毁工做。注意,分离操做是不可逆的。也就是说,咱们没法使一个不可链接的线程变回可链接的状态。不过,对于一个已处于分离状态的线程,执行终止操做仍然会起做用。分离操做由系统调用pthread_detach来执行,它接受一个表明了线程ID的参数值。

一个线程对自身也能够进行两种控制:终止和分离。线程终止自身的方式有不少种。在线程执行的start函数中执行return语句,会使该线程随着start函数的结束而终止。须要注意的是,若是在主线程中执行了return语句,那么当前进程中的全部线程都会终止。另外,在任意线程中调用系统调用exit也会达到这种效果。还有一种终止自身的方式就是显示调用pthread_exit。

而分离pthread_detach函数则是传入本身的TID。

多线程与多进程

在多个线程之间交换线程是很是简单和天然的事,而在多个进程之间只能经过一些额外的手段(好比管道、消息队列、信号量和共享内存区)传递数据。显然,使用这些额外手段会增长开发成本。不过,线程间交换数据虽然简单但却因为可能发生竞态条件而不得不使用一些同步工具(好比互斥量和条件变量)加以保护。这些与业务逻辑无关的代码会增长程序的复杂度,尤为在使用不当的状况下还会引发灾难。

互斥量能够理解为咱们常见的锁。而条件变量所作的就是保证线程间共享的数据状态改变时通知到其余所以而被阻塞的线程。条件变量老是与互斥量组合使用。当线程成功锁定互斥量并访问到共享数据时,共享数据的状态并不必定知足它的要求。下面就经过一个示例来描述条件变量的使用场景。

通用概念

原子操做

执行过程不能中断的操做称为原子操做(atomic operation)。必须一个单一的汇编指令表示,并且须要获得芯片级别的支持。

临界区

临界区(critical section)用来表示一种公共资源或者共享数据,能够被多个线程使用。可是每一次,只有一个线程可使它,一旦临界区资源被占用,其余线程要想使用资源,就必须等待,即串行化访问或执行。

互斥

保证只有一个进程或线程在临界区内的作法只有一个——互斥(mutual exclusion。简称 mutex)。

同步和异步

描述的是用户线程与内核的交互方式:

  • 同步(Synchrounous)是指用户线程发起 I/O 请求后须要等待或者轮询内核 I/O 操做完成后才能继续执行;
  • 异步(Asynchrounous)是指用户线程发起 I/O 请求后仍继续执行,当内核 I/O 操做完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞

描述的是用户线程调用内核 I/O 操做的方式:

  • 阻塞(Blocking)是指 I/O 操做须要完全完成后才返回到用户空间;
  • 非阻塞(Non-Blocking)是指 I/O 操做被调用后当即返回给用户一个状态值,无需等到 I/O 操做完全完成。

一个 I/O 操做其实分红了两个步骤:

  1. 发起 I/O 请求
  2. 实际的 I/O 操做。

阻塞 I/O 和非阻塞 I/O 的区别在于第一步,发起 I/O 请求是否会被阻塞。若是阻塞直到完成那么就是传统的阻塞 I/O ,若是不阻塞,那么就是非阻塞 I/O 。 同步 I/O 和异步 I/O 的区别就在于第二个步骤是否阻塞,若是实际的 I/O 读写阻塞请求进程,那么就是同步 I/O 。

并发(Concurrency)和并行(Parallelism)

并发和并行每每被人所混淆。它们均可以表示两个或多个任务一块儿执行,可是偏重点有些不一样。并发偏重于多个任务交替执行,而多个任务有可能仍是串行。而并行则是真正意义上的“同时执行”。

ConcuarrencyAndParallelism.jpg

严格来讲,并行的多个任务是真实的同时执行,而对并发来讲,这个过程这是交替的,一下子运行任务A一下子执行任务B,系统会不停地在二者间切换。但对于外部观察者来讲,即便多个任务之间是串行并发的,也会形成多任务间是并行执行的错觉。

死锁(DeadLock)、饥饿(Starvation)和活锁(Livelock)

死锁、饥饿和活锁都属于多线程的活跃性问题,若是发生上述状况,那么相关线程可能就再也不活跃,也就是说它可能很难继续往下执行了。

死锁应该是最糟糕的一种状况了,虽然别的状况也没有好到哪儿去。

  • 死锁:多个线程互相等待多方释放资源而一直没有执行。
  • 饥饿:一个或多个线程由于种种缘由没法获取所得的须要资源,致使一直没法执行。致使的缘由每每是当前线程优先级不高致使没有资源,或某线程一直占着关键资源不放。
  • 活锁:多个线程都释放资源给别的线程使用,致使没有线程拿到资源而正常执行。
相关文章
相关标签/搜索