Java锁深入理解2——ReentrantLock

2023/6/5 21:39:12

前言

本篇博客是《Java锁深入理解》系列博客的第二篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结
Java锁深入理解2——ReentrantLock
Java锁深入理解3——synchronized
Java锁深入理解4——ReentrantLock VS synchronized
Java锁深入理解5——共享锁

概述

虽然我们常用的可能是Synchronized,但我们还是先看JDK锁。因为它由JDK实现,有可见的源代码。分析起来会方便一些。

理解了之后,在去看Synchronized,会容易很多(毕竟都是锁,不管是谁实现的,大致的思想应该有共同之处)。

由于后面要从Demo一路深入到JDK源码。而看多线程源码和普通单线程源码还不太一样。如果还没尝试过多线程debug的,可以先看一下Java锁深入理解1——概述及总结,其中讲了如何多线程debug。

Demo1

JDK锁有很多,我们就以最常用的ReentrantLock(可重入锁,也是一种排他锁)来举例

public void testReentrantLock() {
    ReentrantLock mylock = new ReentrantLock();

    mylock.lock();//抢锁 加锁
    System.out.println("------do something....");//线程安全操作
    mylock.unlock();//释放锁
}

在这段demo中,如果有多个线程都会执行这个方法。那么同一时间,只会有一个线程进入到mylock.lock();mylock.unlock();之间。可以在其中做一些需要线程安全的操作。

Demo2

Demo1只是一种最基本的使用方式,通过lock-unlock来圈定一个安全区(也叫临界区),来保证线程安全。

还有两个操作await, signal也挺常见。分别是用来把自己阻塞,把别人唤醒。其实这两个操作对线程安全并没有什么直接作用。已经不属于“解决多线程客观问题”的范畴,而是属于“把多线程玩出更多花样”的范畴。如果说lock-unlock是锁的核心功能,那么await/signal则属于锁的附属功能。

    ReentrantLock mylock = new ReentrantLock();
    Condition c = mylock.newCondition();    

	public void testReentrantLock2() {

        mylock.lock();//抢锁 加锁
        System.out.println("------do something....");//线程安全操作
        try {
            c.await();//把自己阻塞
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        mylock.unlock();//释放锁
    }

    public void testReentrantLock2_1() {

        mylock.lock();//抢锁 加锁
        System.out.println("------do something....");//线程安全操作
        c.signal();//把阻塞的线程唤醒(配合await使用)
        mylock.unlock();//释放锁
    }

Demo2中,首先是增加了Condition c = mylock.newCondition();,不知道怎么翻译。自面意思就是“条件”,一般我们就直接称之为Condition。

语言和语言体系之间必然不可能一一对应。而专业领域的翻译有“精度要求”。当含义误差比较大时,就没必要硬翻译。

此时中文里夹杂英文专业词汇不叫装逼,而是为了表意更准确。(日常表达是没必要的)

testReentrantLock2方法中,在lock-unlock划定的临界区里(这个条件很重要),使用了c.await();。当某个线程A执行到这里的时候,会被阻塞在这里。

此时该线程A会失去锁(它虽然还身在临界区里,但却处于休眠状态)。相当于其他线程忽略线程A的存在,可以继续抢锁。

testReentrantLock2_1中,在lock-unlock划定的临界区里(这个条件很重要),使用了c.signal();。当线程B执行到这里的时候,会把阻塞的线程唤醒(比如上面的线程A)。

此时你可能有个疑问:那如果另一个线程B立马抢到锁,并唤醒A。是不是会和刚醒来的线程A同时身处临界区。

答:是的。 而且如果B使用的是signalAll(),还有可能唤醒一堆被阻塞线程。(所以不要误认为“临界区”同一时间只能有一个线程)

但区别就是:B手中有锁,只要B不出来,其他线程就进不来。而那些被B唤醒的线程能做的 只能默默的把剩下的路走完。

问题

如果用过锁,或许会产生一些疑问:

  • 代码为什么会在mylock.lock()位置停下来
  • 代码为什么会在c.await()位置停下来
  • 抢到锁的本质是什么
  • 怎么保证只有一个线程抢到锁
  • 什么时候才能抢锁

内部机制

下面就正式进入ReentrantLock类的内部,来解答上面的疑惑。

代码结构

在这里插入图片描述

这张图就表示ReentrantLock类的总体结构。(图中并没有严格按照URL的规范画。包含关系直接使用了更直观的嵌套,而不是用线条表示。箭头含义是按规范画的:A—>B表示A继承B)

当new ReentrantLock()时,其实使用的是FairSync(公平锁)或者NonfairSync(非公平锁)。
在这里插入图片描述
也可以通过传参数true,来创建公平锁
在这里插入图片描述

而这两种锁的顶级父类就是AbstrateQueuedSynchronizer(AQS)。

AQS

先整体看一下这个锁的核心类,AQS原理示意图

在这里插入图片描述

这张图相当于图代码结构示意图中,AQS部分的进一步放大,可以看到其中更多丰富的细节。

图中关键的两个东西:一个是state,一个是同步队列

队列中的一个个节点封装着一个个线程。绿色代表是当前获得锁的,在队列中位列第一。后面的黄色节点则处于阻塞状态。AQS就是通过这个队列来管理线程,实现“先来后到”的方式顺序执行。

state是一个标志,相当于一个红绿灯(更像公共厕所的锁上的显示:有人/无人):1表示有线程正在占有锁,其他线程不用白费力气去抢了。0表示当前没有占用,其他线程有机会去抢。当同一个线程在前一个锁还没释放的时候,就又再次抢锁也是可以的,此时state会加到2,以此类推,重入几次,state就是几。

图中的另外一种队列(红色的那种),画了两个,表示这种队列可以有多个(也可以没有)。叫条件队列。也就是代码中,我们使用await之后,线程节点被放置的位置。再被signal唤醒之后,线程节点就从这个红色队列中脱离出来(脱离的优先级也是按照先来后到的方式,从队列头部一个一个的脱落),然后重新回到同步队列中排队。

名词统一

关于AQS中的两种队列的名字,有点乱(有些博客自己都前后不一致)。我根据源码上的注释,给本文统一如下:

等待队列(wait queues):上面两种队列的统称。这两种队列都是有AQS类中的内部类Node类组成的,都是阻塞等待状态(除了同步队列的头节点)。(参考AQS源码中的Node类上的注释的第一句:Wait queue node class)

同步队列(sync queue):也就是实现lock-unlock的核心队列,图中第一条队列。(参考AQS源码中的transferForSignal方法的注释:Transfers a node from a condition queue onto sync queue.)

条件队列(condition queue):就是图中的红色队列。(参考AQS源码中的transferForSignal方法的注释:Transfers a node from a condition queue onto sync queue.)

Transfers a node from a condition queue onto sync queue.意思是:将节点从条件队列转移到同步队列。

Node

上面那个AQS的原理图中,Node只是一个小方块,我们继续放大这个方块,以及两个队列链表

Node节点示意图:
在这里插入图片描述

同步队列示意图:
在这里插入图片描述

条件队列示意图:
在这里插入图片描述

可以看到Node节点之间通过prev和next,组成了同步队列的双向链表。通过nextWaiter,组成了条件队列的单向链表。

线程组织成队列的逻辑场景

那这个两个队列是怎么用Node节点自动组织起来的呢。以同步队列为例介绍一下。

一般情况下,我们会把锁的定义

ReentrantLock mylock = new ReentrantLock();
Condition c = mylock.newCondition();

写在方法外面,因为只需要定义一个即可,后面不需要重复定义。

有了这两句话,我们的AQS容器,以及其中的Condition就生成了。后面只要有线程碰到这个容器,它就像一个高速公路检查站一样,在里面触发一系列的操作。看一下AQS的初始化时的示意图(注意观察和【AQS原理图】的差异):

在这里插入图片描述

在容器里,除了有state之外,还有headtail(组织队列的关键元素)。

当某个线程进来之后,在state的指挥下,被包装成Node节点,然后被head和tail引用。

然后是第二个,它会自动被追加到第一个节点的后面,然后是第三个,第四个,,,

最后就形成了前面我们看到的【AQS原理图】的样子。

可重入锁逻辑

通过上面的介绍,我们基本就掌握了ReentrantLock以及AQS的基本原理。下面是一些源码细节。

流程图

在这里插入图片描述
这个流程图很重要。结合这张图,会帮助理解后面各种操作的逻辑。

lock()

公平锁上锁逻辑

看看不能抢(看state状态,是不是锁定中(其他线程正在运行中))

  • 1-1. 如果能抢,就抢就抢一抢(不断循环尝试)
    • 1-1-1. 抢到了,就把老大给踢出队列(如果有老大的话),自己做老大
    • 1-1-2. 没抢到,自己就阻塞
  • 1-2. 如果不能抢,就进队列去等
    • 1-2-1. 进了队列,发现自己是老二,那么就去尝试抢一抢(进入1-1的循环)
    • 1-2-2. 进了队列,发现自己不是老二,那么就阻塞

解释

  • 老大:也就是头节点,抢到锁的线程。

  • 这里忽略了一些细节:

    • 抢的过程中也可能发现是自己重入(上一次抢到锁的就是自己,现在绕了一圈又进来了),那么也算抢成功(自己是老大,抢完之后还是老大)
    • 等待中:被取消的,会被踢出队列
    • 我们看到类似中断的一些代码,仔细看这些代码,其实并不会引起中断。只是在收到中断信号之后,这个中断信号会唤醒阻塞(但因为在循环里面,所以并不影响结果),然后这个中断信号被抹去,最后又给恢复了(如果感觉有点晕,没关系,你只要知道这个逻辑无伤大雅,不用去刻意理解这部分逻辑,后面会讲到中断这块)。
  • 我们一开始可能认为:一个线程队列,如果简单设计的话。前一个运行完,触发后一个运行似乎是最简单的。

    但实际设计的方案是:老大运行完,确实“通知”老二了。但这个“通知”的意思是:唤醒后一个线程(从阻塞变为非阻塞)。

    就是说:老大退位了,并不意味值老二自动变老大。只是告诉老二,你有权利上位了(上位的过程还是老二主动循环尝试去争取)。

    其实想想也好理解:线程和线程之间都是独立的,没有很强的耦合关系。最大的耦合就是signal唤醒了。

  • 老二怎么踢掉的老大:源码

    setHead(node);
    p.next = null; // help GC

这里的p就是当前节点(老二)的前面的节点(老大)。就是说把老大的next引用指到null。
第一句的setHead方法里,把原本指向前节点的引用指向null。
也就是把双向的引用都断掉。而且把head也指向了老二。老大彻底“失联”,等着被GC回收。

非公平锁上锁逻辑

直接抢抢试试(不去判断state)

  • 1-1. 如果成功,自己直接做老大
  • 1-2. 如果失败,进入“公平锁”流程

unlock()

解锁流程,无论是公平锁还是非公平锁都一样

  1. 把锁的状态改为“非锁定中”
  2. 唤醒下一个节点(unpark)
  3. 从节点上退下来? 【并没有这一步!老大的位置是被老二踢下来的】

await()

  1. 排进条件队列
  2. 释放锁(这一步就是unlock的操作)
  3. 阻塞

signal()

  1. 找到条件队列的第一个节点
  2. 让这个节点从条件队列脱离掉(first.nextWaiter = null;)
  3. 让这个节点排到同步队列的队尾(tail.next = node;)
  4. 唤醒这个节点(unpark)

小结

  • 在AQS中,试图抢锁的只有老大,老二和还未入队列“外来者”,其他节点都处于阻塞状态
  • unlock和await(注意:能做这两个动作的只有拿到锁的头节点),都会调用同一个释放锁的过程(改锁状态为0,唤醒同步队列里的第二个节点)。
    不同点是:await后还会把自己加入条件队列,然后阻塞自己(其实可以说await流程中包含unlock的流程)。
  • 无论是同步队列里的自动阻塞(那些黄色节点),还是使用await后的阻塞(红色节点),本质原理是一样的,都是用park阻塞,都需要被别的线程用unpark唤醒。区别在于:
    • 在哪阻塞:前者在同步队列里阻塞,后者在条件队列里阻塞。
    • 被谁唤醒:前者被头节点释放锁后唤醒,后者被其他线程(其实还是头节点)使用signal唤醒。
  • unlock和signal,都会涉及到唤醒节点(unpark)的操作。
    前者是唤醒的是同步队列里的第二个节点,后着是唤醒条件队列里的第一个节点。
  • 唤醒(unpark):就是是给指定节点“解穴”,让它继续动起来。
  • 被唤醒之后,至于去干什么,取决于线程当前执行到哪了,后面还要做什么。如果是同步队列的节点,被唤醒后就是继续抢锁。而条件队列里的节点,正常就是默默的继续往下执行代码。当然,如果它身处一个循环语句之中,转一圈,它也许还会再次去抢锁。

其实前面的流程已经把取消等流程都给省略了,但还是太细节,太复杂。再画一个更简化版的整体动态概览图(两条实线表示节点的变换位置的方向)
在这里插入图片描述

可能的困惑

  1. lock是为了实现线程安全,那么lock源代码本身的线程安全怎么保证?
    比如:lock()源码中,抢锁(改锁状态),线程入队列都是用了CAS(也是AQS的核心),保证了线程安全。而await的时候在节点入队列时,却直接使用的=,不会出现线程安全问题吗?
    请添加图片描述

答:这是一个思维盲区。或许有读者已经想到问题出在哪了。
因为await只能在lock和unlock之间(临界区)的线程安全区里调用,所以await内不用担心线程安全问题。
整个过程,其实只有抢锁的时候,需要考虑线程安全。后面的操作一直到unlock其实都是线程安全的,其他线程都被阻止在抢锁那一步了。

  1. 线程被唤醒后,在哪复活?是不是像打游戏一样,在泉水(出生地)里复活?

答:这就是想多了。它在哪阻塞,就在哪被唤醒。例如下面的await方法代码
请添加图片描述
线程在LockSupport.park(this);阻塞,那么当它被其他线程唤醒时,就还是从这句话开始执行。
但是,之所以可能引起困惑。从await()开始,到park最终停下,最后再次被唤醒开始往下执行,中间经历了很长的流程,如下图所示:

在这里插入图片描述

这个一维流程图看着晕?再换个二维流程图视角看看:
在这里插入图片描述

我们还看到park这句话被while语句包裹着。也就意味着:即使被唤醒,也又可能立马又阻塞。
这个写法也值得我们学习:线程被唤醒后别晕着头就往下执行,最好看看当前什么状况,如果不能往下执行,也许还得继续阻塞。

  1. 如果锁重入了多次,比如重入了三次,state的值被加到3。此时做await()操作。state值需要清零吗。
    答:不需要,这里的重入就有点像事务,你进了多少层事务,最后都得一层层的出来。除非程序报错。

CAS(compareAndSet)和自旋锁

在说AQS的时候总会有人说CAS和自旋锁。
首先明确一点:CAS本身是不会自旋的,只试一次:返回true或者false
那自旋体现在哪呢,有两段循环语句:

  1. 这是当前线程节点 作为一个“外来节点”(还没排到同步队列里)的接下来的行为:入队
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

代码逻辑:

  • 如果队列的结尾是空(根本没人排队),就去尝试当那第一个节点(也可能尝试失败)。
  • 否则就尝试排到队尾(不一定能排进去)。
  • 这两个条件内的方法都是CAS尝试,如果失败了,就再次循环执行一遍,直到排进去为止。
  1. 这是当前节点,作为同步队列里一个节点,接下来的行为:抢锁或阻塞
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

代码逻辑:

  • 如果前面那个节点是头节点,就抢锁(不一定抢到)
  • 否则就阻塞(等着前面的节点执行完,唤醒我)
  • 循环上面两步,一直到抢到为止

简化代码写法分析及思考

在ReentrantLock源代码中有这么两处典型的if判断语句

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
和

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

以第一段为例,他的逻辑其实是

public final void acquire(int arg) {
        if (!tryAcquire(arg)) {
            if(acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
            
    }

这并不难看出。因为&&的作用,if条件语句中的第一个条件其实也相当于一个判断,对第二个条件的执行与否造成影响。
但如果按照我日常开发的习惯,我基本会写成第二种拆开的写法。甚至写成这样:

public final void acquire(int arg) {
        boolean tryAcquireRes = !tryAcquire(arg);
        if (tryAcquireRes) {
        	//看代码我们就会明白,下面两句话是顺序执行的两句,也给拆开
            Node newWaiter = addWaiter(Node.EXCLUSIVE);
            boolean acquireQueuedRes = acquireQueued(newWaiter, arg);
            if(acquireQueuedRes) {
                selfInterrupt();
            }
                
        }
            
    }

原因无他,只是为了让代码更易读。减少团队合作中的沟通成本,一眼就看出逻辑(这相当于团队之间用代码在沟通)。
但是,这里的写法我是认可的。因为这是在封装工具包,而且是多线程这种对性能要求极高的代码。当然是能多榨取一点性能就多榨取一点。作为开源软件,测试是非常到位的,不担心出bug。


http://www.jnnr.cn/a/369229.html

相关文章

Set的底层实现

一、二分搜索树 最核心的操作在于查找 a.是一棵二叉树 b.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值. c.若它的右子树不为空,则右子树上所有节点的值都大于根节点的值. 特点: 中序遍历为递增排序 二、哈希函数 哈希函数…

01 | Qt基本介绍及环境搭建

1 简介 1.1 简介 Qt 是一个1991年由Qt Company开发的跨平台C图形用户界面应用程序开发框架。它既可以开发GUI程序,也可用于开发非GUI程序,比如控制台工具和服务器。 2014年4月,跨平台集成开发环境Qt Creator 3.1.0正式发布,实现…

C++源码剖析——deque

前言:之前看过侯老师的《STL源码剖析》但是那已经是多年以前的,现在工作中有时候查问题和崩溃都需要了解实际工作中使用到的STL的实现。因此计划把STL的源码再过一遍。   摘要:本文描述了llvm中libcxx的deque的实现。   关键字&#xff1…

REDIS21_缓存双写一致方案、先更新数据库再删除缓存

文章目录①. 什么是缓存双写一致②. 先更新数据库,再更新缓存③. 先删除缓存,再更新数据库④. 先更新数据库,再删除缓存①. 什么是缓存双写一致 ①. 缓存双写一致性,谈谈你的理解 如果redis中有数据,需要和数据库中的值相同如果redis中无数据,数据库中的值要是最新值 ②. 什么…

MarTech老将如何焕发新生?数据猿直播干货分享

‍数据智能产业创新服务媒体——聚焦数智 改变商业MarTech一直备受关注,然而2022年的MarTech市场却不尽如人意。据最新数据显示,2022年,MarTech行业的融资总额为302.43亿元人民币,与2021年相比下降了31.7%。可见后疫情时代&#…

windows通过浏览器访问noVNC(基于web的远程桌面)

目录 一、什么是VNC 和 noVNC? 二、Windows10安装及配置noVNC 2.0、注释 2.1、下载UltraVNC 2.2、下载Node.js 2.3、下载安装git 2.4、创建一个存放文件的文件夹 2.5、安装ws、optimist、mime-types模块(执行websockify.js文件所需) …

双master节点+keepalived方式部署K8s 1.18.20

相关部署方式也挺多,自己采用双master节点单node节点方式,并且采用keepalived部署1.18.20版本,中间也出现过相关小问题,但都一一处理,记录以给需要的同仁们参考,希望大家都可以一起学习交流!&am…

【Git从入门到精通】分支机制

文章目录简述创建新分支切换分支基本的分支与合并操作基本的分支操作基本的合并操作基本的合并冲突解决远程分支推送跟踪分支拉取删除Git的分支模型是Git的杀手锏特性 简述 首先我们来看一下Git是如何存储数据的。 Git通过一系列的快照的方式来存储数据,当你发起提…

聊聊Java线程是个啥东西-Java多线程(1)

为什么要有线程在这个效率和质量并存的时代,首先, "并发编程" 成为 "刚需".单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源. 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其…

【iOS】—— 多线程之pthread、NSThread

文章目录1.pthreadpthread简介:pthread使用方法pthread 其他相关方法2.NSThread创建,启动线程线程相关用法线程相关用法线程状态控制方法线程之间的通信NSThread线程安全和线程同步NSThread 非线程安全NSThread 线程安全线程的状态转换NSThread线程属性n…

Java基础知识——8.字符串及其拓展(完整版)

这篇文章我们来详细的讲一下字符串即String及其拓展。前面一篇也讲了,但是讲的很粗浅,这里我们详细的完整的讲一下,力争讲透String。 目录 1.String类 2.字符串常量池 3.总结 3.1 String类初始化后是不可变的(immutable) 3.2 引用变量与…

【新2023Q2押题JAVA】华为OD机试 - 优秀员工统计

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:优秀员工统计 题目 公司某部…

这几种常见的 JVM 调优场景

假定你已经了解了运行时的数据区域和常用的垃圾回收算法,也了解了Hotspot支持的垃圾回收器。 一、cpu占用过高 cpu占用过高要分情况讨论,是不是业务上在搞活动,突然有大批的流量进来,而且活动结束后cpu占用率就下降了&#xff0…

zlmedaikit android编译

Windows 10 64bit Android Studio:Android Studio Electric Eel | 2022.1.1 Patch 2 NDK: android-ndk-r25c 1. 安装jdk 2. 打开http://ping.chinaz.com网站,输入dl.google.com地址,开始ping监测,选择一个时间最短的大陆IP地址&a…

C语言函数:内存函数memcpy()以及实现

C语言函数&#xff1a;内存函数memcpy() 引言&#xff1a; #define _CRT_SECURE_NO_WARNINGS#include <stdlib.h>int main() {int arr1[20] { 1,2,3,4,5,6,7,8,9 };int arr2[20] { 0 };strcpy(arr2, arr1);return 0; } strcpy函数&#xff1a;C语言函数&#xff1a;字…

Mac升级go版本(指定或最新)

升级流程 在Mac中对go版本的升级采用先卸载后安装的过程进行go版本升级&#xff08;或者回退&#xff09;。 卸载 在卸载前&#xff0c;先查看下当前的go版本&#xff1a; go version 删除 go 目录&#xff1a; sudo rm -rf /usr/local/go /usr/local/go/bin/go /etc/path…

CSS面试题

CSS面试题css画三角形&#xff1a;本质就是利用边框bordercss选择器优先级一个div&#xff0c;没有给高度和宽度&#xff0c;怎么水平垂直居中height与line-height的区别css画三角形&#xff1a;本质就是利用边框border 我们首先看一种情况&#xff1a; width:0px; height:0px…

数据库:Redis哨兵及cluster集群部署

一、redis数据库哨兵模式 目录 一、redis数据库哨兵模式 1、什么是哨兵模式 2、哨兵的作用 3、哨兵结构组成 4、哨兵故障转移机制 5、哨兵工作、切换原理 6、哨兵主节点选举原则 7、哨兵模式部署 二、redis数据库cluster集群 1、cluster集群优点、数据存储及同步方式…

【操作系统复习】第3章 处理机调度与死锁 3

死锁&#xff08;Deadlock&#xff09;&#xff1a;指多个进程在运行过程中因争夺资源而造成的一种僵局&#xff0c;当进程处于这种僵持状态时&#xff0c;若无外力作用&#xff0c;这些进程都将永远不能再向前推进。 对资源不加限制地分配可能导致进程间由于竞争资源而相互制约…

【Docker】1、Docker 基础知识随意介绍

文章目录一、什么是 Docker二、为什么要用 Docker 部署三、Ubuntu Docker 安装四、Dockerfile五、镜像5.1 镜像拉取5.2 镜像删除5.3 使用 docker save 将镜像保存成 tar 归档文件5.4 导入使用 docker save 导出的镜像5.5 使用 docker import 从归档文件中创建镜像5.6 将本地镜像…