lukaliou123 / lukaliou123.github.io

lukaliou123在2022年的面试用知识点总结
Other
5 stars 0 forks source link

Java并发篇-基础 #6

Open lukaliou123 opened 2 years ago

lukaliou123 commented 2 years ago

1.并发编程的三个必要因素

1.原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要买么全部执行成功,要么全部执行失败

2.可见性:一个线程对共享变量的修改,另一个线程能够立刻看到(synchronized,volatile)

3.有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序

补充:并发与并行的区别

简略版: 并发:两个及两个以上的作业在同一 时间段 内执行。 并行:两个及两个以上的作业在同一 时刻 执行。

1.并发是指多个任务交替执行的情况,它们在时间上交错进行,每个任务都有可能在其他任务执行的过程中被暂停和恢复。这类似于人们在同时处理多个任务时的行为,例如同时听音乐、写作业和聊天。

举个例子,假设你正在做一个多人合作的项目,你和你的朋友都负责其中的一部分工作。在并发的情况下,你们会通过交替执行来完成任务,例如你先做一段时间,然后让你的朋友接替,然后再由你接手,如此循环。这种方式可以提高整体的效率,但是任务的执行顺序可能不确定。

2.并行是指多个任务同时进行的情况,每个任务都在独立的处理器或处理单元上执行,彼此之间没有交替和依赖。这类似于人们同时处理多个任务时,每个任务都有自己的资源和工具,彼此之间互不干扰。

再举个例子,假设你和你的朋友各自拥有一台电脑,你可以同时进行各自的工作,互不干扰。你可以同时写作业,而你的朋友可以同时听音乐,彼此之间不需要等待或交替执行。

简而言之,并发是指多个任务在时间上交错执行,而并行是指多个任务同时进行。并发适用于在单个处理器或处理单元上实现多个任务的情况,而并行适用于利用多个独立的处理器或处理单元同时执行多个任务的情况

为什么多线程编程被称为并发而不是并行?

其实,并发和并行都是处理多任务的策略,只是它们的侧重点不同。并行是一种物理概念,意味着多个任务在物理上同时执行,它依赖于多处理器或者多核心。而并发是一种更为逻辑的概念,它表示系统具有处理多个任务的能力,不论这些任务是否真的在同一时刻被执行。

关于多线程编程,尽管多个线程可能在同一时刻进行(在多核或者多处理器的环境下),但其本质还是并发的表现。因为线程的调度、切换都是由操作系统决定的,操作系统通过上下文切换使得我们有一种多个线程“同时”在运行的错觉,但是实际上在单核处理器中,任意时刻只有一个线程在执行,它们是交替执行的

所以说,我们将多线程编程称为并发编程,是因为多线程的本质是交替执行,即使在多核环境下多个线程可能真的在同一时刻执行,那也只是并发的一个特殊表现,而并发作为更为通用的概念,更适合用来描述多线程编程。

为什么要用多线程?

1.进程切换是一个开销很大的操作,线程切换的成本较低。 2.线程更轻量,一个进程可以创建多个线程。多个线程可以并发处理不同的任务,更有效地利用了多处理器和多核计算机。而进程只能在一个时间干一件事,如果在执行过程中遇到阻塞问题比如 IO 阻塞就会挂起直到结果返回。 3.同一进程内的线程共享内存和文件,因此它们之间相互通信无须调用内核

先从总体上来说: 1.从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

2.从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

2.再深入到计算机底层来探讨1.单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率

2.多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。

lukaliou123 commented 2 years ago

2.何为进程? 何为线程?

1.进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。个进程都有自己独立的内存空间,这个空间包括代码、运行时数据、堆等。因为进程间的内存空间是独立的,所以一个进程不能直接访问另一个进程的资源。进程也有各自独立的系统资源(如文件句柄、设备)和一个或多个线程 2.线程与进程相似,但线程是一个比进程更小的执行单位。是操作系统任务调度(CPU调度的基本单位。线程是在进程之内创建的,一个进程内部的所有线程共享相同的内存空间。这就意味着,线程间的通信比进程间的通信更快,更高效,但同时也意味着一个线程的错误可能影响到同一进程内的其他线程。

3.线程和进程区别

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程相反

4.什么是上下文切换

多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换 概括:当前任务在执行完CPU时间片切换到另一个任务之前,会先保存自己的状态,以便再次切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

补充:进程和处理器的关系

一个处理器(CPU)可以运行多个进程,而一个进程也可以在多个处理器上运行。让我逐一解释:

一个处理器运行多个进程:这是操作系统任务调度的基础。处理器会在各个进程之间快速切换,给我们一种并行运行的错觉。但实际上,任何给定的时间点,只有一个进程在处理器上运行

一个进程在多个处理器上运行:这个稍微复杂一些,通常在多核处理器的环境中,我们会有多线程的进程。这个进程中的各个线程可以被分配到不同的处理器(或核心)上运行。但是要注意,单个线程无法同时在多个处理器上运行。

补充:什么是协程?

协程,也叫微线程或纤程,是一种用户态的轻量级线程。协程的调度完全由用户控制。协程相比于线程的最大优势在于其"轻量级",可以快速创建大量的协程,并且在内存和切换上有明显优势

简单来说,协程是一种比线程更轻量级的存在,由于其简单的创建和切换方式,使得在某些情况下,使用协程可以获得比线程更好的性能和更简洁的代码。协程尤其在IO密集型应用中表现优秀,比如Web服务器,因为协程可以在IO等待期间让出CPU给其他协程使用

线程和协程相比,各自的使用场景和优点

1.线程的优势:

1.线程是由操作系统内核进行调度的,因此可以利用多核CPU的并行计算能力,对于CPU密集型的任务,多线程可以有效提高程序的执行效率

2.线程编程模型相对成熟,大多数编程语言都原生支持多线程编程,且有大量的线程同步、通信等机制。(Java目前就没有原生协程库)

3.线程可以很好的处理阻塞式IO,可以在等待IO的时候做其他事情。

2.协程的优势:

1.协程是在用户态进行切换的,因此切换开销小,性能消耗低。对于需要频繁切换的场景(比如I/O密集型任务),协程有很大优势。

2.协程调度由程序员自己控制,更灵活。可以根据程序的需求,自由地在合适的时机进行切换。相对容易避免死锁之类的

3.协程更容易编写异步非阻塞型的代码,可以简化编程模型。

总的来说,线程和协程各有优势,适用于不同的场景。在CPU密集型的场景下,多线程可以充分利用多核CPU的计算能力,提高程序的执行效率。而在I/O密集型的场景下,协程因其低廉的切换成本和易于编写异步代码的特性,有很大的优势

lukaliou123 commented 2 years ago

5.什么是线程死锁

1.死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成一种堵塞的现象,若无外力作用,它们都将无法推进下去。这种永远互相等待的进程被称为死锁进程/线程 2.多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程被无限期的阻塞,因此程序不可能正常终止。

6.形成死锁的四个必要条件是什么

(1) 互斥条件(Mutual Exclusion):一个资源每次只能被一个进程使用。

(2) 请求与保持条件(Hold and Wait):一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件(No Preemption):进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件(Circular Wait):若干进程之间形成一种头尾相接的循环等待资源关系

举例: 想象两个人在厨房:一个人需要同时使用刀和叉来准备晚餐,另一个人需要使用叉和刀来准备甜点。这两个人对刀和叉都有“互斥”的使用权,每次只能一个人使用。

假设第一个人拿到了刀,同时第二个人拿到了叉。然后他们都等待对方使用完另一个他们需要的工具。这就产生了一个“环路等待”的情况:第一个人等待第二个人释放叉,第二个人等待第一个人释放刀。由于他们都在等待,所以没有人释放他们手中的工具,这就是死锁。

那么,如果我们打破这四个条件中的任何一个,就可以防止死锁的发生。比如,我们可以规定一次只能请求一个资源,或者我们可以允许资源的剥夺,或者我们可以使用某种协议来避免环路等待。

7.如何解决死锁

解决死锁的方法解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。 1.预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。 2.避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生 3.检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。 4.解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来

其中避免有涉及大名鼎鼎的银行家算法(也许可以作为亮点) https://blog.csdn.net/qq_33414271/article/details/80245715

6.1.一个经典的死锁的例子

image

一个很经典的死锁例子是两个线程,每个线程都需要两把锁才能进行,但是线程1已经拿到锁A,线程2已经拿到锁B,然后他们同时请求另一把锁,这就会造成死锁。 6NQX34F@6 P{L925_WO%MA3 1690345146147 在这个例子中,Thread1获得了lock1,然后尝试获取lock2。同时,Thread2获得了lock2,然后尝试获取lock1。这就导致了一个循环等待的情况,也就是我们通常说的死锁。

lukaliou123 commented 2 years ago

8.创建线程的四种方式

1.继承Thread类: image

2.实现Runnable接口: image

3.实现Callable接口 image

4.使用匿名内部类方式 image

补充:为什么一般不是继承Thread?

因为Java只能继承一个抽象类,如果继承了Thread,不方便做其他修改,不如实现后两个接口

lukaliou123 commented 2 years ago

9.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

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

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

lukaliou123 commented 2 years ago

10.线程的生命周期和状态

image 1.NEW: 初始状态,线程被创建出来但没有被调用 start() 。

2.RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

3.BLOCKED阻塞状态,需要等待锁释放。

4.WAITING等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

5.TIME_WAITING超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

6.TERMINATED:终止状态,表示该线程已经运行完毕。

lukaliou123 commented 2 years ago

11.线程的状态(废弃):

  1. NEW:初始状态,线程被构建,但是还没有调用start()方法
  2. RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中”
  3. BLOCKED:阻塞状态,表示线程阻塞于锁
  4. WAITING:等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程作出一些特定动作(通知或中断)
  5. TIME_WAITING:超时等待状态,它是可以在指定的时间自行返回地
  6. TERMINATED:终止状态,表示当前线程已经执行完毕
lukaliou123 commented 2 years ago

12. sleep() 和 wait() 有什么区别和共同点?

共同点: 两者都可以暂停线程的执行。

区别: 1.sleep() 方法没有释放锁,而 wait() 方法释放了锁 。

2.wait() 通常被用于线程间交互/通信sleep()通常被用于暂停执行

3.wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法

4.sleep()方法执行完成后,线程会自动苏醒,或者也可以使用 wait(long timeout) 超时后线程会自动苏醒。

5.sleep() 是 Thread 类的静态本地方法,wait() 则是 Object 类的本地方法。为什么这样设计呢?

lukaliou123 commented 1 year ago

13.线程间的通信方式

1.共享内存:这是最直接、最简单的线程间通信方式。多个线程读、写同一个共享变量,通过这个共享变量实现线程间的通信。当然了,多个线程同时读写共享变量需要进行同步控制。

3.通过管道进行通信:管道是一种比较传统的通信方式,它可以在不同线程之间或者进程之间,提供一个通信的通道。在Java中有PipedInputStream和PipedOutputStream两种管道。

4.通过BlockingQueue阻塞队列进行通信:Java 5开始引入的阻塞队列,实现了生产者-消费者模式,生产者存放元素到队列,消费者从队列取元素。如果队列为空,消费者线程会被阻塞等待;如果队列满了,生产者线程会被阻塞等待。

13.1..线程间的同步方式

1.wait() / notify() 方法(事件):这是一种等待/通知机制,他们是Object的两个方法。通过这两个方法配合,可以实现一个线程在等待某个条件成立,而另外的线程在条件成立时通知这个线程继续运行。 BSV2~1RRN85`T4$W{3CPFAL 0H C%D)4RB%K3K%CCC~IXNC 在这个例子中,我们创建了两个线程,一个等待线程和一个通知线程。等待线程会在同步块中调用wait()方法,进入等待状态。而通知线程则在同步块中调用notify()方法,唤醒因调用wait()方法在该对象监视器上等待的线程。

注意,wait()、notify()和notifyAll()方法是Object类的方法,必须在同步块或者同步方法中调用,因为这些方法需要获取对象的监视器。

2.信号量(Semaphore):主要做同步,有时候也能做进程间的互斥。 例子: U$0M2YPL()B3I6ZQQLZA{YY Y`$GE9XG}}E4OQI4PPS CVV GZYFWREY3 PB74QVJEBJX)8 6ISM~0NL~3BTP7}NM M0QZG 4O1OI8SC4{RH@~NV_O8BXGY

3..信号(Signal):类似于进程间的信号处理。 S}RS_SU W82BZCR_X_DEZYB M@0X7B$H{T CG1HDR(W%QE 2}2{T@KAM} IL%M%KWL9T~S

3.互斥锁(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。

4.读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。

5.屏障(Barrier):屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的 CyclicBarrier 是这种机制。 障是一种同步机制,主要是为了让一组线程在一个预定的执行点集合,直到所有线程都到达该点后才能继续执行。

Java 中有一个类叫做 CyclicBarrier,它就是实现屏障的一个工具。当创建 CyclicBarrier 时,你要指定一个整数,代表要同步的线程数量。当一个线程到达屏障点,它将调用 CyclicBarrier 的 await() 方法。这个线程将被阻塞,直到所有其他的线程都到达屏障点。一旦所有线程都调用了 await(),所有线程都将继续执行

补充:线程间通信和同步的区别

线程间的通信和同步实际上是两个不同的概念,但它们在多线程编程中通常是密切相关的。

1.线程间通信是指在多线程环境下,不同的线程之间如何进行数据共享,或者说如何将信息从一个线程传递到另一个线程。这可以通过共享内存(比如共享变量)或者通过消息传递(比如使用队列)等方式实现。

2.而线程同步是指在多线程环境下,如何协调线程的执行顺序或者状态,以防止因为线程间的竞态条件(race condition)而导致的数据不一致或者其它的错误情况。这通常可以通过使用互斥量(mutex)、信号量(semaphore)、屏障(barrier)等工具实现。

尽管这两者是不同的概念,但在实践中,线程间的通信往往需要使用某种形式的同步机制来保证正确性,因此这两个概念经常同时出现。例如,当两个线程需要通过共享变量来通信时,通常需要使用互斥量来同步对该共享变量的访问,以防止竞态条件。

lukaliou123 commented 1 year ago

14.i++和++i哪个线程不安全,为什么

于i++和++i的问题,实际上它们都不是线程安全的。

我们知道,无论是i++还是++i,它们都包含三个操作:读取i的值、对i加1、然后将加1后的值赋给i。这就意味着,如果两个线程同时进行i++或者++i操作,可能会出现读取相同的i值,然后分别加1并赋值给i,结果就比预期要小了。这就是所谓的"线程不安全"。

你可以这样理解,假设我们的i初始值为0,然后两个线程同时对i进行i++操作。理想情况下,我们希望操作完成后i的值变为2,但实际上可能只会变为1。因为两个线程可能都读取到i为0,然后都对0加1,然后都将1赋值给i,导致i最后的值只为1。这就是线程不安全。

所以,无论是i++还是++i,在多线程情况下,如果没有加以同步措施(比如使用锁等同步机制),都是线程不安全的。

lukaliou123 commented 1 year ago

15.如何让线程按顺序执行?

使用Java的 join() 方法来确保线程的顺序执行。这个方法会使得启动该方法的线程在被 join() 的线程完成后再执行。

下面是一个简单的示例,我们有三个线程:Thread1、Thread2 和 Thread3,我们希望它们按照 Thread1 -> Thread2 -> Thread3 的顺序执行: 1689702985166 在上面的代码中,每个线程都会在前一个线程完成后开始运行。请注意,调用 join() 方法可能会抛出 InterruptedException,所以需要进行相应的异常处理。

同时,这种方式只适合你知道线程顺序并且线程数量不多的情况,对于大量线程的顺序控制,需要考虑更加复杂的线程协作机制,比如使用 java.util.concurrent 包下的工具类。

lukaliou123 commented 1 year ago

16.JUC原子性怎么实现

Java的并发库java.util.concurrent (JUC)提供了一组原子类,这些原子类用于实现无锁的线程安全操作。原子操作是不能被线程调度机制中断的操作;也就是说,一旦线程开始了原子操作,那么在原子操作完成之前,线程不会被切换出去。这个特性确保了即使是在无锁的情况下,也能够正确执行多线程并发操作。

原子类的底层是通过使用CAS(Compare and Swap)操作来实现原子性的。CAS是一种无锁算法,在硬件层面上保证了操作的原子性。CAS在操作值的时候,会比较当前值和预期值,如果当前值等于预期值,则执行操作,否则不执行。通过这种方式,CAS确保了原子操作的完成。 1689703221747 在这段代码中,compareAndSet(current, next) 会尝试将当前值设为 next,这个操作是原子的。如果当前值 current 不等于实际的值(有其他线程改变了值),那么 compareAndSet 返回 false,循环继续;否则返回 true,方法返回。

lukaliou123 commented 1 year ago

17.解决死锁的方法(详细)

预防,避免,检测和解除四种。 1.预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。 2.避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生 3.检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。 4.解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来

17.1.死锁的预防

死锁四大必要条件上面都已经列出来了,很显然,只要破坏四个必要条件中的任何一个就能够预防死锁的发生。 破坏第一个条件 互斥条件:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源 往往是不能同时访问的 ,所以这种做法在大多数的场合是行不通的。

破坏第三个条件 非抢占:也就是说可以采用 剥夺式调度算法,但剥夺式调度方法目前一般仅适用于 主存资源 和 处理器资源 的分配,并不适用于所有的资源,会导致 资源利用率下降

所以一般比较实用的 预防死锁的方法,是通过考虑破坏第二个条件和第四个条件1、静态分配策略 静态分配策略可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源(有点类似事务),不会出现占有一些资源等待一些资源的情况。

静态分配策略逻辑简单,实现也很容易,但这种策略 严重地降低了资源利用率,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才使用的,这样就可能造成一个进程占有了一些 几乎不用的资源而使其他需要该资源的进程产生等待 的情况。

2、层次分配策略 层次分配策略破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略,证明略。

一个例子: 假设我们有四个进程P1,P2,P3和P4,它们依次持有A,B,C和D层次的资源,并且P1正在等待B,P2正在等待C,P3正在等待D,P4正在等待A,这样就形成了一个循环等待。

但是如果我们应用层次分配策略,这个循环等待就不可能发生。因为根据层次分配策略,一个进程在申请了某层次的资源后,只能去申请更高层次的资源。所以P1在申请了A后,只能去申请B,C或D,不能再去申请A;同样,P2在申请了B后,只能去申请C或D,不能再去申请A或B;P3在申请了C后,只能去申请D,不能再去申请A,B或C;P4在申请了D后,不能再去申请A,B,C或D。这样就破坏了循环等待的可能,避免了死锁。

lukaliou123 commented 1 year ago

17.2.死锁的避免

上面提到的 破坏 死锁产生的四个必要条件之一就可以成功 预防系统发生死锁 ,但是会导致 低效的进程运行 和 资源使用率 。而死锁的避免相反,它的角度是允许系统中同时存在四个必要条件 ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 明智和合理的选择 ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。 我们将系统的状态分为 安全状态不安全状态 ,每当在未申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。 如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁

那么如何保证系统保持在安全状态呢? 通过算法,其中最具有代表性的 避免死锁算法 就是 Dijkstra 的银行家算法,银行家算法用一句话表达就是:当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 真的分配资源给该进程。

银行家算法详情可见《一句话+一张图说清楚——银行家算法》open in new window 。 操作系统教程书中讲述的银行家算法也比较清晰,可以一看. 死锁的避免(银行家算法)改善了 资源使用率低的问题 ,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 安全性检查 ,需要花费较多的时间image 首先是银行家算法中的进程: 包含进程Pi的需求资源数量(也是最大需求资源数量,MAX) 已分配给该进程的资源A(Allocation) 还需要的资源数量N(Need=M-A

Available为空闲资源数量,即资源池(注意:资源池的剩余资源数量+已分配给所有进程的资源数量=系统中的资源总量) image

怎么样算安全?

"安全"的定义是:存在至少一条安全序列(即一个进程的执行顺序),使得系统中的每个进程都可以依次得到资源并成功执行完毕

具体来说,银行家算法的步骤如下:

1.当一个进程申请资源时,检查申请的资源数量(NEED)是否超过其最大需求(MAX),如果超过,则拒绝请求;否则,进行下一步。

2.系统试探性地分配资源给进程,然后更新系统的资源状态。

3.使用安全性算法来检查新的系统状态是否安全。如果安全,就真正分配资源;否则,撤销试探性分配,并让进程等待。

安全性算法的步骤如下

1.找出一个尚未执行完但已有足够资源执行完的进程。

2.假设这个进程可以顺利执行完并释放它所占用的资源。

3.重复上述两步,直到所有进程都被假设执行完毕。如果存在这样的执行序列,则系统状态是安全的。

lukaliou123 commented 1 year ago

17.3.死锁的检测

对资源的分配加以限制可以 预防和避免 死锁的发生,但是都不利于各进程对系统资源的充分共享。解决死锁问题的另一条途径是 死锁检测和解除 (这里突然联想到了乐观锁和悲观锁,感觉死锁的检测和解除就像是 乐观锁 ,分配资源时不去提前管会不会发生死锁了,等到真的死锁出现了再来解决嘛,而 死锁的预防和避免 更像是悲观锁,总是觉得死锁会出现,所以在分配资源的时候就很谨慎)。 这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统 定时地运行一个 “死锁检测” 的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它

进程-资源分配图

操作系统中的每一刻时刻的系统状态都可以用进程-资源分配图来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于检测系统是否处于死锁状态

用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,每个键进程用一个圆圈表示,用 有向边 来表示进程申请资源和资源被分配的情况

图中 2-21 是进程-资源分配图的一个例子,其中共有三个资源类,每个进程的资源占有和申请情况已清楚地表示在图中。在这个例子中,由于存在 占有和等待资源的环路 ,导致一组进程永远处于等待资源的状态,发生了 死锁

image

进程-资源分配图中存在环路并不一定是发生了死锁。因为循环等待资源仅仅是死锁发生的必要条件,而不是充分条件。图 2-22 便是一个有环路而无死锁的例子。虽然进程 P1 和进程 P3 分别占用了一个资源 R1 和一个资源 R2,并且因为等待另一个资源 R2 和另一个资源 R1 形成了环路,但进程 P2 和进程 P4 分别占有了一个资源 R1 和一个资源 R2,它们申请的资源得到了满足,在有限的时间里会归还资源,于是进程 P1 或 P3 都能获得另一个所需的资源,环路自动解除,系统也就不存在死锁状态了

死锁检测步骤

知道了死锁检测的原理,我们可以利用下列步骤编写一个 死锁检测 程序,检测系统是否产生了死锁。 1.如果进程-资源分配图中无环路,则此时系统没有发生死锁 2.如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁。 3.如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 既不阻塞又非独立的进程 ,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内 消除所有的边 ,则不会发生死锁,否则会发生死锁。(消除边的过程类似于 拓扑排序)

17.4.死锁的解除

当死锁检测程序检测到存在死锁发生时,应设法让其解除,让系统从死锁状态中恢复过来,常用的解除死锁的方法有以下四种: 1.立即结束所有进程的执行,重新启动操作系统:这种方法简单,但以前所在的工作全部作废,损失很大。 2.撤销涉及死锁的所有进程,解除死锁后继续运行:这种方法能彻底打破死锁的循环等待条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算。 3.逐个撤销涉及死锁的进程,回收其资源直至死锁解除。 4.抢占资源:从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除