PancrasL的博客

关于Java并发的总结和思考

2021-06-15

img

1. 进程、线程和协程的基本概念

1.1 什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位。

1.2 什么是线程

线程是程序的一条执行路径,是现代操作系统调度的基本单位。

在Java中,同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈

img

1.3 什么是协程

协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。

img

2. 进程和线程

2.1 进程和线程的关系

进程是系统运行程序和资源分配的基本单位,线程是系统调度的基本单位。

  • 进程是指一个具有一定独立功能的程序关于某个数据集合的一次运行活动

  • 区别:

    • 程序是指令的有序集合,是一个静态概念
    • 进程是一个能独立运行的单位,能与其他进程并行地活动
    • 线程是竞争计算机系统有限资源的基本单位,也是进行处理机调度的基本单位。
image-20210721170532276

2.2 程序计数器为什么是私有的?

每个线程拥有不同的执行路径,因此需要程序计数器来记录当前执行到的位置,保证线程切换后能恢复到正确的执行位置。

2.3 虚拟机栈和本地方法栈为什么是私有的?

每个线程的方法拥有自己独立的栈帧,以保证线程中的局部变量不被别的线程访问到。

2.4 堆和方法区

堆和方法区为进程所有,由线程共享。

堆主要用于存放新创建的对象;方法区主要用去存放编译后的代码等信息(例如已被加载的类信息、常量、静态变量)

2.5 线程的生命周期和状态

img

2.6 线程的上下文切换

当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次 再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换

3. 并发和并行

3.1 并发和并行的区别

  • 并发:同一时间段内,多个任务都执行了(例如时间片法)
  • 并行:同一时刻,执行多个任务(例如多核)

4. 多线程

4.1 为什么使用多线程?

  • 从计算机底层来说: 计算机有多任务调度的需求,最早是基于进程实现的多任务调度,但是进程切换成本较高,由此诞生了更加轻量的线程,满足多任务调度需求。
  • 从互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

4.2 使用多线程带来的问题

  • 内存泄漏
  • 一致性问题(线程同步)
  • 死锁

4.3 Java和线程

4.3.1 内核线程实现

使用内核线程实现的方式也被称为1:1实现。内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核 (Multi-Threads Kernel)。

内核线程(Kernel-Level Thread,KLT)

轻量级进程(Light Weight Process,LWP)

一个进程(P)拥有多个线程(LWP),每一个LWP被映射为KLT,由线程调度器调度到物理CPU上执行。

image-20210720195629494

4.3.2 用户线程实现

即协程

5. 上下文切换

当线程发生中断,或者用完当前时间片,就会触发上下文切换,操作系统会将CPU的使用权分配给其他线程。

保护现场、恢复现场。

6. 死锁

定义:在两个或多个并发进程中,如果每个进程持有某种资源而又都等待着别的进程释放它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁

6.1 产生死锁的必要条件

  1. 互斥条件:在一段时间内某资源仅为一个进程所占有。
  2. 不可剥夺条件:该资源只能由获得该资源的进程自己来释放。
  3. 占有并等待条件:进程在等待新资源的同时,继续占用已分配到的资源。
  4. 循环等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中下一个进程所请求。

6.2 死锁的处理

  • 预防死锁:通过设置某些限制条件,去破坏死锁四个必要条件中的一个或多个
  • 避免死锁:在资源动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁的产生
  • 检测死锁:允许系统在运行过程中发生死锁,但可检测死锁的发生,并采取适当措施加以清除
  • 解除死锁:当检测出死锁后,采取措施将进程从死锁状态中解脱出来

6.2.1 预防死锁

  • 破坏互斥条件:采用虚拟分配技术排除非共享设备死锁的可能性。

  • 破坏不可剥夺条件:

    • 允许资源抢占
  • 破坏占有并等待:

    • 一次性资源分配方案
    • 每个进程申请资源前,释放它所占有的资源
  • 破坏循环等待条:静态资源分配、有序资源分配、修复死锁、重启系统。

6.2.2 避免死锁

方法:

  • 有序资源分配法:资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),必须以上升的次序申请资源。

  • 银行家算法:系统给当前进程分配资源时,先检查是否安全,如果安全,允许分配资源,否则让进程进入等待状态。

技术:

  • 加锁顺序:线程按照一定的顺序加锁,要求事先知道所有可能会用到的锁,并对这些锁做适当的排序,但总有些时候是无法预知的。
  • 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
  • 死锁检测:死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

6.2.3 检测死锁

6.2.4 解除死锁

  • 资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
  • 撤销进程法。强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
  • 进程回退法。让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

7. Java线程方法

7.1 sleep() 和 wait()

  • 两者最主要的区别在于:**sleep() 方法没有释放锁,而 wait() 方法释放了锁** 。
  • 两者都可以暂停线程的执行。
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

7.2 start() 和 run()

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

Reference
[1]: https://snailclimb.gitee.io/javaguide/#/docs/java/multi-thread/2020%E6%9C%80%E6%96%B0Java%E5%B9%B6%E5%8F%91%E5%9F%BA%E7%A1%80%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93
[2]: https://blog.csdn.net/wljliujuan/article/details/79614019