Skip to content

5.1 进程、线程基础知识

  • 并行和并发
    • 并发:单核处理器交替处理多个任务
    • 并行:多核处理器同时处理多个任务
    • 现代多核处理器并发和并行共存
  • 进程和线程:
    • 进程:
      • 是资源管理的基本单位,一个进程一个独立的资源
      • 创建或撤销进程时,系统都要为之分配或回收系统资源,开销大
      • 唯一标识:PCB(process control block)
        • 包含如资源占用,进程名称,进程状态等信息
      • 父子进程:一个进程 fork 出另一个进程,子进程可以继承父进程所有资源
        • 会把主进程的页表复制一份给子进程,两者虚拟空间不同,但物理空间相同,省空间
        • 但是子进程页表对应的属性是只读,如果发生写操作,会触发缺页中断,进行物理内存复制,重新映射,并设置为可读写,即写时复制(copy-on-write)
        • 子进程通过 PPID 标识父进程
      • 孤儿进程:父进程被终止,子进程变成孤儿进程,被 1 号进程管理
        • 孤儿进程只有自己被终止才会停,1 号进程不会主动杀死它,因为它可能还在执行任务
      • 1 号进程:守护进程,负责系统启动等
      • 僵尸进程:子进程退出后等待父进程进行资源回收的状态,如果父进程一直不回收会浪费资源
        • 如果父进程直接退出,会交由 1 号进程管理,1 号进程会进行回收
        • 可以双 fork,A 进程 fork B 进程,B 进程 fork C 进程,杀死 B 进程,即 A 进程的子进程全交由 1 号进程管理,不用担心僵死进程,且不会破坏 copy on write
        • top 命令可以看到 zombie 就是僵死进程
    • 线程:
      • 程序执行的基本单位,上下文切换比进程快(不需要切换共享部分,如虚拟内存)
      • 多个线程共享一个进程的资源
        • 主要是堆
        • 程序计数器、栈私有,因为每个线程有自己的执行上下文
      • 内核级线程:依赖于内核进行管理
        • 轻量级进程(LWP):由内核支持用户线程,每个LWP 和线程一一对应
      • 用户级线程:程序管理生命周期,内核不直接参与,比内核线程轻量,无需内核态和用户态切换
      • 用户级线程和内核线程可以有:一对一,多对一,多对多的关系
      • 对于 C 语言而言,一个线程崩溃会导致所属进程的所有线程崩溃,即程序终止,对 Java 不会,因为虚拟机捕获了
        • 因为多个线程共享地址空间,某个线程对地址的非法访问可能导致危险操作,操作系统认为这个很严重,就直接让其崩溃
        • 因此游戏不建议使用多线程,否则一个用户挂了,其他用户也会崩溃
        • Java 虚拟机如果收到 kill -15,会优雅退出,先进行资源清理
        • -9 JVM 也无法捕获恢复,会立刻退出,不优雅
    • 状态:
      • 创建、就绪、运行、阻塞、结束
    • 临界区:一次仅允许一个进程使用的资源称为临界资源,一般通过锁解决

进程和线程通信

进程间通信常见方式

  • 管道(pipe):
    • 一种半双工的通信方式,数据只能单向流动
    • 匿名管道:
      • 是特殊文件只存在于内存,没有存在于文件系统中,
      • 只存在于父子关系的进程间通信,随着进程的消亡而消亡,
      • 如shell 命令中的「|
    • 有名管道:
      • 是一种类型为 p 的设备文件
      • 允许无亲缘关系进程间的通信
  • 信号量(semophore) :
    • 数据是无格式的字节流
    • 一个计数器,表示的是资源个数,通过 P/V操作
    • 常作为一种锁机制,确保任何时刻只能有一个进程访问共享资源
    • 实现访问的互斥性,或者进程间的同步(同步指多进程按照特定顺序进行执行)
  • 信号(signal):
    • 异步通信机制
    • 可以在应用进程和内核之间直接交互
    • 硬件来源:如键盘 Cltr+C
    • 软件来源:如 kill 命令
      • 其中 SIGKILLSIGSTOP 是必定停止
    • 其他信号进程可以按照人为预设:
      1. 执行默认操作
      2. 捕捉信号
      3. 忽略信号
  • 消息队列(messagequeue):
    • 保存在内核的「消息链表」
    • 消息体是可以用户自定义的数据类型,发送接收保持一致
    • 每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程,会有较大的延迟
  • 共享内存(shared memory):
    • 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
    • 需要注意多进程竞争同个共享资源会造成数据的错乱
  • 套接字(socket):
    • 实现不同主机的进程间通信

线程间通信常见方式

  • 锁机制:包括互斥锁、条件变量、读写锁
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
    • 互斥方式:同时只有一个线程访问共享资源(A 和 B 不能同时执行)
    • 同步方式:线程间顺序可保证(先执行 A,再执行 B)
    • 操作:
      • P 操作,将 sem-1,如果 < 0,阻塞,是在进入临界区之前
      • V 操作,将 sem+1,如果 >= 0,唤醒一个等待中的进程/县城,是在离开临界区后
  • 信号机制(Signal):类似进程间的信号处理
  • 全局变量:多线程共享同进程的资源

用户态和内核态

  • 用户态(User Mode) :可以直接读取用户程序的数据。执行某些特殊权限如读写磁盘需要向操作系统发起系统调用请求,进入内核态。
  • 内核态(Kernel Mode):几乎可以访问任何资源,权限很高,很危险,所以不能交由程序直接操作。
    • 如果没有用户态,所有进程共享系统资源,导致资源冲突很大。
  • 两者切换成本较高,需要进行一系列的上下文切换和权限检查
  • 切换时机:
    • 系统调用:用户态进程主动要求切换,如读取磁盘
    • 中断:外围设备如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
    • 异常:用户态程序发生异常如缺页异常,会切换到内核态处理。
      • 中断和异常处理类似,但是中断来自处理器外部,异常是执行当前指令的结果

死锁

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件;
    • 该资源同时只能由一个线程持有
    • 避免方式:
      • 把“独占”资源改成可共享副本(copy-on-write)
      • CAS/版本号的乐观锁
  • 持有并等待条件;
    • 持有的资源在想要获取其他资源时不会释放
    • 避免方式:超时放弃资源
  • 不可剥夺条件;
    • 线程只有在使用完资源后才会释放,其他线程无法抢占
    • 避免方式:
      • 优先级剥夺(高优先级可以抢占)
      • 超时放弃
  • 环路等待条件;
    • 多个线程获取资源的顺序构成了环形链
    • 避免方式:多个线程获取资源顺序相同

利用工具排查死锁问题

如果你想排查你的 Java 程序是否死锁,则可以使用 jstack/jconsole 工具,它是 jdk 自带的线程堆栈分析工具。

什么是悲观锁、乐观锁?

悲观锁:先质疑,悲观锁是修改共享数据前,都要先加锁,防止竞争。

  • 互斥锁
    • 加锁失败,线程会释放CPU
      • 线程会睡眠
      • 锁被释放后会被唤醒
      • 需要切换到内核态,会有两次线程上下文切换,开销较大
  • 自旋锁
    • 加锁失败,线程会忙等待
    • 自旋锁基于 CAS 加了 while 或者睡眠 CPU 的操作而产生自旋的效果,加锁失败会忙等待直到拿到锁
    • 用户态完成加锁和解锁操作,不会主动产生线程上下文切换,开销较小
    • 最好是使用 CPU 提供的 PAUSE 指令而不是while循环来实现「忙等待」,降低成本。
    • 如果单核CPU,可能导致CPU一直空转(需要强占式调度器)
  • 高并发锁竞争记录,通常用于写比较多的情况
  • 如果确定被锁住的代码执行时间短,采用自旋锁,否则互斥锁
  • 上面是锁的最基本处理方式,读写锁中的写锁基于上面中的一个进行实现

乐观锁:先相信,乐观锁是先修改完共享资源,再验证这段时间内有没有发生冲突

  • 如果没有,那么操作完成
  • 如果有,操作失败,重试
  • 适用于读多写少
  • 实现方案:
    • 版本号:数据修改次数,修改前获取该值,修改时原子确认该值相等才更新
      • 时间戳类似,对于 mysql 可以直接使用 mtime
    • CAS(compare and swap):
      • 设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID
      • CAS(V, E, N):V 变量值,E 预期值,N 新值,V 等于 E 时更新为 N,如果不等相当于其他线程修改过,放弃更新
        • CAS(lock, 0, pid) 如果 lock 为 0,设置 lock 为 pid,表示自旋锁的加锁操作
        • CAS(lock, pid, 0) 如果 lock 为 pid,设置 lock 为 0,则表示解锁操作。
      • ABA 问题:V 经过两次修改回到了 E,但是当前 pid 认为其没有被修改过
        • 和版本号、时间戳配合使用
      • 由于一般采用自旋重试等待,可能给 CPU 带来很大的开销

在线文档就是使用乐观锁,否则多人无法同时打开文档

公平锁和非公平锁有什么区别?

  • 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
  • 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

正在精进