Java 并发相关的一些概念


平时在学习中遇到的一些并发相关的概念, 做个笔记整理一下。

1 相关概念

1.1 同步 VS 异步

同步和异步关注的是 消息通信机制

同步: 调用方发起一个调用, 在没有得到结果之前, 这个调用就不返回, 即调用方主动等待调用的结果。
(A 调用了一个方法, A 必须等到这个方法返回结果了, 才能执行后续的代码)

异步: 调用方发起一个调用后, 就不管了, 后续有结果, 可以让被调用方通知调用方 (A 调用了一个方法后, 就继续执行后面的代码, 不用等待方法的返回值)。
一般情况下, 被调用方处理完后, 会通过状态、通知、回调函数等来通知调用者。

1.2 阻塞 VS 非阻塞

阻塞和非阻塞关注的是 程序在等待调用结果时的状态

阻塞: 调用方发起一个调用, 在没有得到结果之前, 调用方会被挂起, 无法进行其他的操作, 等待执行结果。

非阻塞: 调用方发起一个调用, 在没有得到结果之前, 调用方不会被挂起。

1.3 并发 VS 并行

并发: 多个任务交替执行, 也就是同一个时刻只有一个任务在执行。

并行: 多个任务同时执行, 也就是同一个时刻有多个任务在执行。

1.4 进程 VS 线程 VS 协程

进程是程序向操作系统申请资源 (如内存空间和文件句柄) 的基本单位。也可以理解为是线程的一个容器。

线程也叫轻量级进程, 是程序执行的最小单位, 本身只拥有少部分执行必须的资源。是进程中可独立运行的最小单位。

协程也叫微线程, 用户可以自己控制协程切换的时机, 不再需要陷入系统的内核态。和线程的区别: 线程间的切换都是由系统进行的, 但是协程可以由代码显示进行调度。

1.5 用户态 VS 内核态

用户态和内核态是操作系统的两种运行级别, 两者最大的区别就是特权级不同。 运行在用户态的程序不能直接访问操作系统内核数据结构和程序, 而运行在内核态的程序则没有限制。

Linux 的架构中, 很重要的一个能力就是操纵系统资源的能力。但是系统资源是有限的, 无限制的使用, 将会导致浪费, 同时错误地使用会导致错误。
为了减少资源浪费, Linux 制定了一个等级制定, 即特权。Linux 将特权分成两个层次, 以 0 和 3 标识, 0 的特权级要高于 3。 0 特权级在操纵系统资源上是没有任何限制的, 可以执行任何操作, 而 3 特权则会受到极大的限制。
特权级 0 称之为内核态, 特权级 3 称之为用户态。

内核态和用户态之间的转换方式有 3 种

  1. 系统调用: 如调用 write(), read(), send() 等 IO 函数等操作, 进程需要会进入内核态使用内核代码去完成操作。系统调用本身就是中断, 软件中断, 和硬件中断不同
  2. 异常: 当前进程运行在用户态, 如果这个时候发生了异常事件, 就会触发切换
  3. 外设中断: 当外设完成用户的请求时, 会向 CPU 发送中断信号

1.5.1 用户空间 VS 内核空间

操作系统将虚拟内存划分为两部分:
一部分是用户空间, 供各个进程使用, 在这部分空间内各个进程之间相互独立。
另一部分是内核空间, 只提供给供内核使用, 每个进程可以通过系统调用进入内核, 所以内核空间由系统内的所有进程共享。

在 Linux 系统中, 用户空间和内核空间所占的虚拟内存比例是 3:1, 用户空间占了内存的低位, 内核空间占据内存的高位。

可以简单的理解为: 进程工作在用户空间就是用户态, 进程工作在内核空间就是内核态。

1.5.2 零拷贝

传统的磁盘读取

  1. 应用程序发起系统调用, 进入内核态
  2. 从磁盘中读取数据到内核空间
  3. 将数据从内核空间拷贝到用户空间
  4. 应用程序恢复为用户态

零拷贝的实现

  1. 应用程序发起系统调用, 进入内核态
  2. 从磁盘读取数据到内核空间, 再将内核空间的数据映射到用户空间, 内核空间和用户空间不需要拷贝了
  3. 应用程序恢复为用户态

2 独占锁和共享锁

2.1 定义

独占锁: 锁同一时刻只能被一个线程持有。
共享锁: 锁同时可以被多个线程持有。

2.2 具体的实现

读锁是共享锁的具体实现, 而写锁是独占锁的具体实现。

读锁可以在没有写锁的时候被多个线程同时持有, 而写锁是独占的, 写锁的优先级要高于读锁。

2.3 锁的附加属性

2.3.1 公平性

锁的获取是否是公平的, 即锁的获取是否按照申请是顺序获取。

如果像排队一样, 先来的先获取锁, 后来的后获取锁, 具备公平性, 这就是公平锁。
如果锁的获取是没有规则的, 可能后到的线程先获取锁, 这就是不公平锁。

2.3.2 可重入性

同一个线程在已经获取到一个锁了, 后续在未释放的情况下又一次申请同一个锁

  1. 申请成功, 锁具备可重入性
  2. 申请失败, 锁不具备可重入性

2.4 锁引起的问题

2.4.1 死锁

两个或者更多的线程相互占用对方的资源的锁, 而又相互等对方释放锁而被永远暂停 (线程的生命周期状态为 BLOCKED或者 WAITING)。

2.4.2 活锁

线程拿到资源却又相互释放不执行, 相互谦让, 都主动将资源释放给别的线程使用, 这样这个资源在多个线程之间跳动而又得不到执行。

2.4.3 线程饥饿

线程一直无法获得其所需的资源而导致其任务一直无法进展 (优先级高的线程一直抢占优先级低线程的资源, 导致低优先级线程无法得到执行)。

3 并发的优缺点

3.1 优点

  1. 充分利用多核 CPU 的运算能力, 使线程之间可以并行操作, 更快的处理能力
  2. 异步执行比同步执行更快, 使程序的处理能力得到提升
  3. 资源利用率提高, 如文件读取等情况

3.2 缺点

1. 频繁的上下文切换
在操作系统内部, 多线程直接的实现是基于时间片实现的。 CPU 基于时间片不断的切换线程, 使得少量的 CPU 能够支撑起大量的线程。但是每次切换线程为了能够
在下次轮到这个线程的时间片到了能够继续执行下去, 需要把当前的状态保存起来, 下次好恢复。而这个过程是很耗时间的, 所以大量的线程存在, 会导致更高的上下
文切换, 反而会影响到系统的效率。

2. 数据准确性问题
多个线程共享数据时可能会产生于期望不相符的结果

3. 线程活性问题
死锁, 活锁, 线程饥饿等问题

4 参考

浅谈用户态和内核态以及用户空间和内核空间
并发编程基础概念
原来 8 张图, 就可以搞懂「零拷贝」了
NIO效率高的原理之零拷贝与直接内存映射


  目录