目录
一、进程
- 字面意思可以理解成“正在进行的程序”,这样子的话可以把进程当成是一个动态的,程序是一个静态的。
- 好比视频播放器(一个程序),不点击播放按钮,它不会“动”,就是一串安静的代码,当你双击时它就会“动”,视频播放器(程序)就通过进程“动”起来了。
- 进程怎么做到的?
- 进程会利用操作系统的调度器分配给它的 CPU 时间片,通过 CPU 来执行代码(注意:现代操作系统都是直接调度线程,不会调度进程哦)
操作系统给进程分配了 CPU 时间片资源,在代码执行过程,还需要存储一些
数据(如程序代码本身就需要先存储起来。代码执行过程中的变量、常量这些存储在数据段,堆中,还有栈中。),
所以进程还分配有内存空间资源。
- Q:从上面可以知道线程都共享进程的什么资源?
A:分配给进程的资源,绝大部分都是线程间共享的。比如内存空间的代码段,数据段,堆。比如文件描述符等。而栈则是每个线程特有的,因为线程是程序执行的最小单位,它需要记录自己的局部变量等。
- 进程就是分配资源的基本单位
“进程就是程序的实例(就像面向对象编程中的类,类是静态的,只有实例化后才运行,且同一个类可以有多个实例)”
二、线程
回到第一个例子,如果一个视频很大,我们想看个这个视频,要等视频播放器 加载、解码完整视频-->才能播放 ,这样效率很慢,我们可以想到可以一边加载解码一边播放,这样不会浪费时间空等。
- 所以我们可以让 一个进程(让视频动起来)里边有好几个线程(线程1:加载解码视频,线程2:播放)
- 还有另一种方法就是有2个进程(进程1:加载和解码视频,进程2:播放)
但是这个方法有什么缺点吗?
首先得知道:进程间是完全独立的,互不干扰。而线程则共享同一个进程的资源,
所以线程间交换数据更方便,几乎没有通讯损耗。但进程间交换数据就麻烦多了,得通过一些通讯机制,比如管道、消息队列之类的。
所以方法2pass
- 我们都知道资源有分配,就会有冲突。而且线程的执行时机由操作系统调度,程序员无法控制。
- 但大部分情况下线程之间还是可以和平共处的,但有一种情况,就是大家都想对同个资源进行写操作时,就会发生覆盖,导致数据不一致等问题。
- 解决方法有很多种,比如加锁方案,无锁方案等。
- 例子
public class Main { // 定义一个静态成员变量 a private static int a = 1; // 定义一个方法 add 来增加 a 的值 public static void add() { a += 1; } public static void main(String[] args) { add(); System.out.println("a 的值是: " + a); // 输出 a 的值 } }
a 是个静态成员变量,它存储在进程内存空间的数据段,共享于多个线程,所以它属于线程间共享的资源。
add
方法的逻辑a += 1;
步骤一:获取 a 变量的值
步骤二:执行 +1 运算
步骤三:将运行结果赋值给 a
- 如果线程 1 在执行完步骤一和步骤二,还没执行步骤三时,操作系统进行了 CPU 调度,发生了线程切换,使得线程 2 也开始执行步骤一和步骤二。
- 接下来线程 1 和线程 2 都会各自执行步骤三。
- 因为 add 方法执行了两次,正确的结果 a 的值应该是 +2。但结果是 +1。这样的结果有时候会让你摸不着头脑,而不稳定的结果也将会导致应用的不稳定。
- Q:从上面的例子我们知道 “操作系统进行了 CPU 调度,发生了线程切换” 那线程切换到底发生了什么呢?
- A:线程切换会进行线程上下文切换。线程在运行时,实际上是在执行代码,而执行代码过程中需要存储一些中间数据,也可能会执行一些 I/O 操作。 如果过程中被中断,需要得保留现场,以便下次恢复继续运行。
- Q:线程切换具体都存储些什么呢?
- A:首先是下一个要执行的代码,这个存储在程序计数器中。然后是一些中间数据如局部变量等,会存储在线程栈中,所以线程栈指针也需要保存。。为了加速计算,中间数据中对当前指令执行至关重要的部分会存储在寄存器中。
线程死锁
1、什么是线程死锁?
线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
产生死锁的四个必要条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,
而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己
已经获取的资源。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
如何检测死锁?
使用
jmap
、jstack
等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,jstack
的输出中通常会有Found one Java-level deadlock:
的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top
、df
、free
等命令查看操作系统的基本情况,出现死锁可能会导致CPU、内存等资源消耗过高。如何预防死锁?
破坏死锁的产生的必要条件即可:
破坏请求与保持条件:一次性申请所有的资源。
破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称
<P1、P2、P3.....Pn>
序列为安全序列。
三、协程
- 还是第一个例子,我们之前一个线程负责运行加载和解码逻辑,另一个线程负责播放逻辑,对吧?
- 其实还有优化的空间。线程在执行加载视频片段时,必须等待结果返回才能执行解码操作。
- 这样加载片段的等待时间似乎又被浪费了,协程所能发挥的作用让我们可以充分利用这段时间。只需让线程在加载的同时进行解码,就能大幅减少加载等待的时间。
- Q:但是我们只要用不同的线程分别处理加载和解码,也能达到同样的效果。
- A:这样做可以是可以,但多线程会带来一些问题。“首先,一个线程用于执行加载操作,这主要是 I/O 操作,几乎不消耗 CPU 资源,导致该线程长时间处于阻塞状态,这是很浪费的。当然,你可以让它休眠以释放 CPU 时间,但创建线程本身就有开销,线程切换同样有开销。相比之下,协程非常轻量,创建和切换的开销极小”
- Q:为什么协程的创建和切换的开销极小呢?
- A:主要是因为它并非操作系统层面的东西,就不涉及内核调度。一般是由编程语言来实现,它属于用户态。
- Q:那协程不会有像多线程那样的资源覆盖问题吗?
- A:线程的执行时机由操作系统调度,程序员无法控制,这正是多线程容易出现资源覆盖的主要原因。而协程的执行时机由程序自身控制,不受操作系统调度影响,因此可以完全避免这类问题。
- 此外,同一个线程内的多个协程共享同一个线程的 CPU 时间片资源,它们在 CPU 上的执行是有先后顺序的,不能并行执行。协程不能并行执行,而线程是可以并行执行的。
- Q:协程能并发执行:在一个线程内并发执行多个任务
- A:协程则可以在执行到一半时暂停。利用这一特性,我们可以在遇到 I/O 这类不消耗 CPU 资源的操作时,将其挂起,继续执行其他计算任务,充分利用 CPU 资源。等 I/O 操作结果返回时,再恢复执行。