进程管理 - 进程、线程状态,死锁
进程、线程
为什么用线程
最重要的原因是可以线程之间可以共享进程的地址空间
比如一个软件,有三个不同的功能模块,需要并发执行,如果使用多进程的方式,那么就会存在一些问题:
- 进程之间的数据如何共享
- 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;
因此就有了线程,线程之间可以并发运行且共享相同的地址空间。
线程的优点:
- 一个进程中可以同时存在多个线程;
- 各个线程之间可以并发执行;
- 同一个进程的各个线程之间可以共享地址空间和文件等资源;
线程与进程的比较
- 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
- 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
- 线程能减少并发执行的时间和空间开销;
线程相比进程能减少开销,体现在:
- 线程的创建时间比进程快,因为进程在创建的过程中,还需要内存管理信息等,而线程在创建的过程中,则不需要
- 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享)。而对于进程之间的切换,切换的时候需要切换地址空间,而这个切换过程开销是比较大的;
进程的上下文切换和线程的上下文切换:
- 进程的上下文切换包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
- 现成的上下文切换:
- 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
- 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
进程线程在JVM层面的区别
- 进程是操作系统中资源分配的最小单位,是独立的执行环境。在JVM中,一个Java应用程序实际上是一个独立的进程。每个Java进程都有自己独立的JVM实例,包括独立的Java堆、方法区、程序计数器等。由于每个Java进程都是独立的,它们之间不共享内存空间。如果需要在不同的Java进程之间共享数据,需要通过一些通信机制,如网络通信、文件共享等。
- 在JVM中,每个Java应用程序至少有一个线程,即主线程,用于执行main方法。除了主线程外,Java程序还可以创建额外的线程来执行并发任务。在JVM中,每个线程都会拥有独立的程序计数器(Program Counter,PC)、栈(Stack)和本地方法栈(Native Stack)。它们共享堆内存和方法区(Method Area),这样多个线程之间可以共享对象和类信息。
进程间的通信方式
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
线程间的通信方式呢?
同个进程下的线程之间都是共享进程的资源,只要共享变量都可以做到线程间通信,比如全局变量,所以对于线程间关注的不是通信方式,而是关注多线程竞争共享资源的问题,信号量也同样可以在线程间实现互斥与同步:
管道
管道,就是内核里面的一串缓存。管道只能一端写入,另一端读出。通信数据都遵循先进先出原则
管道这种通信方式效率低,不适合进程间频繁地交换数据。
消息队列
对于上面的问题,消息队列的通信模式就可以解决。
比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。
但是消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程
共享内存
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。解决了以上消息队列提出的问题
但是,如果多个进程同时修改同一个共享内存,就有可能发生冲突
信号量
所以,为了防止多进程竞争共享资源,造成的数据错乱,就需要保护机制,因此就有了信号量机制。信号量机制就是常说的PV操作:
- P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
- V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。
信号
上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号跟信号量没什么关系
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
- 执行默认操作。Linux 对每种信号都规定了默认操作,例如,SIGTERM 信号,就是终止进程的意思。
- 捕捉信号。可以为信号定义一个信号处理函数,当信号发生时,就执行相应的信号处理函数。
- 忽略信号。当不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程
Socket
前面提到方式都是在同一台主机上进行进程间通信,那要想**跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。**当然了,Socket也可以同主机上进程通信
可根据创建 Socket 的类型不同,分为三种常见的通信方式,
- 基于 TCP 协议的通信方式
- 基于 UDP 协议的通信方式
- 本地进程间通信方式
线程的几种状态
- 新建
当用new关键字创建一个线程时,还没调用start 就是新建状态。 - 就绪
调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。 - 运行
当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。 - 阻塞
当遇到以下几种情况,线程会从运行状态进入到阻塞状态。- 调用sleep方法,使线程睡眠。
- 调用wait方法,使线程进入等待。
- 当线程去获取同步锁的时候,锁正在被其他线程持有。
- 调用阻塞式IO方法时会导致线程阻塞。
- 调用suspend方法,挂起线程,也会造成阻塞。
需要注意的是,阻塞状态只能进入就绪状态,不能直接进入运行状态。因为,从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定。只有当线程获得了CPU时间片之后,才会进入运行状态。
- 死亡
当run方法正常执行结束时,或者由于某种原因抛出异常都会使线程进入死亡状态。另外,直接调用stop方法也会停止线程。但是,此方法已经被弃用,不推荐使用。
死锁
什么是死锁?
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁只有同时满足以下四个条件才会发生:
- 互斥条件;
- 持有并等待条件;
- 不可剥夺条件;
- 循环等待条件
死锁的四个条件
互斥条件;
互斥条件是指多个线程不能同时使用同一个资源。
如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。
持有并等待条件
持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
不可剥夺条件
不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
循环等待条件
环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环形图。
预防/避免死锁
如何预防死锁
(不允许死锁发生,属于静态策略)
产生死锁的四个必要条件是:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
那么预防死锁问题就只需要破环其中一个条件就可以,大多数场景下,互斥条件和不可剥夺条件是无法破坏的。
最常见的并且可行的就是使用资源有序分配法,来破环循环等待条件;或者使用定时锁来打破持有并等待条件。
资源有序分配法
线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
也就是说,线程 A 获取资源的顺序是先获取互斥锁 A,然后获取互斥锁 B;那就让线程B也先获取互斥锁A,再获取互斥锁B。
定时锁
线程在加锁时可指定 timeout 参数,该参数指定超过 timeout 秒后会自动释放锁定的资源,这样就可以解开死锁了。
如何避免死锁
(不允许死锁发生,属于动态策略)
是一种依靠算法机制来实现的死锁预防机制,它主要是针对那些不可能实现按序加锁,也不能使用定时锁的场景的。
比如银行家算法,核心思想就是:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。
参考链接
来源:https://www.xiaolincoding.com/ ,Seven进行了部分补充完善