跳至主要內容

进程管理 - 进程、线程状态,死锁


进程、线程

为什么用线程

最重要的原因是可以线程之间可以共享进程的地址空间

比如一个软件,有三个不同的功能模块,需要并发执行,如果使用多进程的方式,那么就会存在一些问题:

  1. 进程之间的数据如何共享
  2. 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销 PCB;进程切换时,保存当前进程的状态信息;

因此就有了线程,线程之间可以并发运行且共享相同的地址空间。

线程的优点:

  1. 一个进程中可以同时存在多个线程;
  2. 各个线程之间可以并发执行;
  3. 同一个进程的各个线程之间可以共享地址空间和文件等资源;

线程与进程的比较

  1. 进程是资源(包括内存、打开的文件等)分配的单位,线程是 CPU 调度的单位;
  2. 进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;
  3. 线程能减少并发执行的时间和空间开销;

线程相比进程能减少开销,体现在:

  1. 线程的创建时间比进程快,因为进程在创建的过程中,还需要内存管理信息等,而线程在创建的过程中,则不需要
  2. 线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
  3. 同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享)。而对于进程之间的切换,切换的时候需要切换地址空间,而这个切换过程开销是比较大的;

进程的上下文切换和线程的上下文切换:

  1. 进程的上下文切换包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
  2. 现成的上下文切换:
    • 当两个线程不是属于同一个进程,则切换的过程就跟进程上下文切换一样;
    • 当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;

进程线程在JVM层面的区别

  1. 进程是操作系统中资源分配的最小单位,是独立的执行环境。在JVM中,一个Java应用程序实际上是一个独立的进程。每个Java进程都有自己独立的JVM实例,包括独立的Java堆、方法区、程序计数器等。由于每个Java进程都是独立的,它们之间不共享内存空间。如果需要在不同的Java进程之间共享数据,需要通过一些通信机制,如网络通信、文件共享等。
  2. 在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,则表明当前没有阻塞中的进程;

信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

信号

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。

信号跟信号量没什么关系

信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。

  1. 执行默认操作。Linux 对每种信号都规定了默认操作,例如,SIGTERM 信号,就是终止进程的意思。
  2. 捕捉信号。可以为信号定义一个信号处理函数,当信号发生时,就执行相应的信号处理函数。
  3. 忽略信号。当不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程

Socket

前面提到方式都是在同一台主机上进行进程间通信,那要想**跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。**当然了,Socket也可以同主机上进程通信

可根据创建 Socket 的类型不同,分为三种常见的通信方式,

  1. 基于 TCP 协议的通信方式
  2. 基于 UDP 协议的通信方式
  3. 本地进程间通信方式

线程的几种状态

  1. 新建
    当用new关键字创建一个线程时,还没调用start 就是新建状态。
  2. 就绪
    调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。
  3. 运行
    当线程获得CPU时间片后,就会进入运行状态,开始执行run方法。
  4. 阻塞
    当遇到以下几种情况,线程会从运行状态进入到阻塞状态。
    • 调用sleep方法,使线程睡眠。
    • 调用wait方法,使线程进入等待。
    • 当线程去获取同步锁的时候,锁正在被其他线程持有。
    • 调用阻塞式IO方法时会导致线程阻塞。
    • 调用suspend方法,挂起线程,也会造成阻塞。
      需要注意的是,阻塞状态只能进入就绪状态,不能直接进入运行状态。因为,从就绪状态到运行状态的切换是不受线程自己控制的,而是由线程调度器所决定。只有当线程获得了CPU时间片之后,才会进入运行状态。
  5. 死亡
    当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进行了部分补充完善

seven97官方微信公众号
seven97官方微信公众号