声明:本文是自己自学慕课网悟空老师的《Java并发核心知识体系精讲》的死锁部分后整理而成课程笔记。

课程链接如下:https://coding.imooc.com/class/362.html

如有侵权,请私信我并第一时间删除本文。

1. 死锁是什么?有什么危害

1.1什么是死锁

  • 发生在并发中

  • 互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进, 导致程序陷入无尽的阻塞,这就是死锁。

  • 一图胜千言

  • 多个线程造成死锁的情况

如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁

1.2 死锁的影响

死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力

◆数据库中:检测并放弃事务

◆JVM中:无法自动处理

2. 发生死锁例子

2.1 最简单的死锁

/*** 描述:     必定发生死锁的情况*/
public class MustDeadLock implements Runnable {​int flag = 1;
​static Object o1 = new Object();static Object o2 = new Object();
​public static void main(String[] args) {MustDeadLock r1 = new MustDeadLock();MustDeadLock r2 = new MustDeadLock();r1.flag = 1;r2.flag = 0;Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();}
​@Overridepublic void run() {System.out.println("flag = " + flag);if (flag == 1) {synchronized (o1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("线程1成功拿到两把锁");}}}if (flag == 0) {synchronized (o2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("线程2成功拿到两把锁");}}}}
}

分析

  • 当类的对象flag=1时(T1) ,先锁定O1,睡眠500毫秒, 然后锁定O2 ;
  • 而T1在睡眠的时候另一个flag=0的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放01 ;
  • T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定;
  • T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定;
  • T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。

注意看退出信号: Process finished with exit code 130(interrupted by signal 2: SIGINT) ,是不正常退出的信号,对比正常结束的程序的结束信号是0

2.2 实际生产中的例子:转账

  • 需要两把锁

  • 获取两把锁成功,且余额大于0 ,则扣除转出人,增加收款人的余额,是原子操作

  • 顺序相反导致死锁

    /*** 描述:     转账时候遇到死锁,一旦打开注释,便会发生死锁*/
    public class TransferMoney implements Runnable {​int flag = 1;static Account a = new Account(500);static Account b = new Account(500);static Object lock = new Object();
    ​public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 1;r2.flag = 0;Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();t1.join();t2.join();System.out.println("a的余额" + a.balance);System.out.println("b的余额" + b.balance);}
    ​@Overridepublic void run() {if (flag == 1) {transferMoney(a, b, 200);}if (flag == 0) {transferMoney(b, a, 200);}}
    ​public static void transferMoney(Account from, Account to, int amount) {synchronized (from){/**加上sleep就造成死锁了try {Thread.sleep(500);      } catch (InterruptedException e) {e.printStackTrace();}*/  synchronized (to){if (from.balance - amount < 0){System.out.println("余额不足,转账失败。");}from.balance -= amount;to.balance += amount;System.out.println("成功转账" + amount + "元");}}
    ​
    ​}
    ​static class Account {​public Account(int balance) {this.balance = balance;}
    ​int balance;
    ​}
    }
    

    未加注释代码部分,运行结果如下

加上注释后运行结果如下,由于请求锁的顺序相反,造成相互等待,互不退让,造成死锁。

2.3 模拟多人随机转账

  • 5万人很多,但是依然会发生死锁,墨菲定律
  • 复习:发生死锁几率不高但危害大
import java.util.Random;
​
/*** 描述:     多人同时转账,依然很危险*/
public class MultiTransferMoney {​private static final int NUM_ACCOUNTS = 500;private static final int NUM_MONEY = 1000;private static final int NUM_ITERATIONS = 1000000;private static final int NUM_THREADS = 20;
​public static void main(String[] args) {​Random rnd = new Random();Account[] accounts = new Account[NUM_ACCOUNTS];for (int i = 0; i < accounts.length; i++) {accounts[i] = new Account(NUM_MONEY);}class TransferThread extends Thread {@Overridepublic void run() {for (int i = 0; i < NUM_ITERATIONS; i++) {int fromAcct = rnd.nextInt(NUM_ACCOUNTS);int toAcct = rnd.nextInt(NUM_ACCOUNTS);int amount = rnd.nextInt(NUM_MONEY);transferMoney(accounts[fromAcct], accounts[toAcct], amount);}System.out.println("运行结束");}}for (int i = 0; i < NUM_THREADS; i++) {new TransferThread().start();}}public static void transferMoney(Account from, Account to, int amount) {synchronized (from){synchronized (to){if (from.balance - amount < 0){System.out.println("余额不足,转账失败。");}from.balance -= amount;to.balance += amount;System.out.println("成功转账" + amount + "元");}}}
​static class Account {​public Account(int balance) {this.balance = balance;}
​int balance;
​}
}

运行结果,发生死锁

3. 死锁的4个必要条件(缺一不可)

1.互斥条件

每个资源每次只能被一个线程(或进程,下同)使用。

为什么资源不能同时被多个线程或进程使用呢?这是因为如果每个人都可以拿到想要的资源,那就不需要等待,所以是不可能发生死锁的。

2.请求与保持条件

一个线程因请求资源而阻塞时,则需对已获得的资源保持不放。

如果在请求资源时阻塞了,并且会自动释放手中资源(例如锁)的话,那别人自然就能拿到我刚才释放的资源,也就不会形成死锁。

3.不剥夺条件

指线程已获得的资源,在未使用完之前,不会被强行剥夺。

比如我们数据库,它就有可能去强行剥夺某一个事务所持有的资源,这样就不会发生死锁了。所以要想发生死锁,必须满足不剥夺条件,也就是说当现在的线程获得了某一个资源后,别人就不能来剥夺这个资源,这才有可能形成死锁。

4.循环等待条件

只有若干线程之间形成一种头尾相接的循环等待资源关系时,才有可能形成死锁。

比如在两个线程之间,这种“循环等待”就意味着它们互相持有对方所需的资源、互相等待;而在三个或更多线程中,则需要形成环路,例如依次请求下一个线程已持有的资源等

4. 如何定位死锁

4.1 jstack

4.2 死锁检测工具类(ThreadMXBean)

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
​
/*** 描述:     用ThreadMXBean检测死锁*/
public class ThreadMXBeanDetection implements Runnable {​int flag = 1;
​static Object o1 = new Object();static Object o2 = new Object();
​public static void main(String[] args) throws InterruptedException {ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();r1.flag = 1;r2.flag = 0;Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();Thread.sleep(1000);//以下是核心代码ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if (deadlockedThreads != null && deadlockedThreads.length > 0) {for (int i = 0; i < deadlockedThreads.length; i++) {ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);System.out.println("发现死锁" + threadInfo.getThreadName());}}//以上是核心代码}
​@Overridepublic void run() {System.out.println("flag = " + flag);if (flag == 1) {synchronized (o1) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o2) {System.out.println("线程1成功拿到两把锁");}}}if (flag == 0) {synchronized (o2) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (o1) {System.out.println("线程2成功拿到两把锁");}}}}
}

5. 修复死锁的策略

5.1 线上发生了死锁怎么办

线上问题都需要防患于未然,不造成损失地扑灭几乎已经是 不可能

保存案发现场然后立刻重启服务器

◆暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版

5.2 常见的修复策略

  • 死锁避免策略:哲学家就餐的换手方案、转账换序方案
  • 检测与恢复策略: 一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁。
  • 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复

5.2.1 死锁避免策略(包含哲学家就餐问题)

  • 思路:避免相反的获取锁的顺序

  • 转账时避免死锁

  • 实际上不在乎获取锁的顺序

    代码演示

     public class TransferMoney implements Runnable {int flag = 1;static Account a = new Account(500);static Account b = new Account(500);static Object lock = new Object();public static void main(String[] args) throws InterruptedException {TransferMoney r1 = new TransferMoney();TransferMoney r2 = new TransferMoney();r1.flag = 1;r2.flag = 0;Thread t1 = new Thread(r1);Thread t2 = new Thread(r2);t1.start();t2.start();t1.join();t2.join();System.out.println("a的余额" + a.balance);System.out.println("b的余额" + b.balance);}@Overridepublic void run() {if (flag == 1) {transferMoney(a, b, 200);}if (flag == 0) {transferMoney(b, a, 200);}}public static void transferMoney(Account from, Account to, int amount) {class Helper {public void transfer() {if (from.balance - amount < 0) {System.out.println("余额不足,转账失败。");return;}from.balance -= amount;to.balance = to.balance + amount;System.out.println("成功转账" + amount + "元");}}int fromHash = System.identityHashCode(from);int toHash = System.identityHashCode(to);if (fromHash < toHash) {synchronized (from) {synchronized (to) {new Helper().transfer();}}}else if (fromHash > toHash) {synchronized (to) {synchronized (from) {new Helper().transfer();}}}else  {    //通过hashcode来决定获取锁的顺序、冲突时需要“加时赛”,有主键更方便synchronized (lock) {synchronized (to) {synchronized (from) {new Helper().transfer();}}}}}
    

避免策略:哲学家就餐的换手方案、转账换序方案

有死锁和资源耗尽的风险

◆死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)

/*** 描述:     演示哲学家就餐问题导致的死锁*/
public class DiningPhilosophers {​public static class Philosopher implements Runnable {​private Object leftChopstick;
​public Philosopher(Object leftChopstick, Object rightChopstick) {this.leftChopstick = leftChopstick;this.rightChopstick = rightChopstick;}
​private Object rightChopstick;
​@Overridepublic void run() {try {while (true) {doAction("Thinking");synchronized (leftChopstick) {doAction("Picked up left chopstick");synchronized (rightChopstick) {doAction("Picked up right chopstick - eating");doAction("Put down right chopstick");}doAction("Put down left chopstick");}}} catch (InterruptedException e) {e.printStackTrace();}}
​private void doAction(String action) throws InterruptedException {System.out.println(Thread.currentThread().getName() + " " + action);Thread.sleep((long) (Math.random() * 10));}}
​public static void main(String[] args) {Philosopher[] philosophers = new Philosopher[5];Object[] chopsticks = new Object[philosophers.length];for (int i = 0; i < chopsticks.length; i++) {chopsticks[i] = new Object();}for (int i = 0; i < philosophers.length; i++) {Object leftChopstick = chopsticks[i];Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];        philosophers[i] = new Philosopher(leftChopstick, rightChopstick);           new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();}}
}

哲学家问题的多种解决方案

  • 服务员检查(避免策略)

  • 改变一个哲学家拿叉子的顺序(避免策略)

    /*** 描述: 其他不变,修改main函数*/public static void main(String[] args) {Philosopher[] philosophers = new Philosopher[5];Object[] chopsticks = new Object[philosophers.length];for (int i = 0; i < chopsticks.length; i++) {chopsticks[i] = new Object();}for (int i = 0; i < philosophers.length; i++) {Object leftChopstick = chopsticks[i];Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];//以下为不同部分if (i == philosophers.length - 1) {philosophers[i] = new Philosopher(rightChopstick, leftChopstick);} else {philosophers[i] = new Philosopher(leftChopstick, rightChopstick);}//以上为不同部分new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();}}
    }
    
  • 餐票(避免策略)

  • 领导调节(检测与恢复策略)

5.2.2 死锁的检测与恢复策略

检测算法:锁的调用链路图

◆允许发生死锁

◆每次调用锁都记录

◆定期检查"锁的调用链路图”中是否存在环路

◆一旦发生死锁,就用死锁恢复机制进行恢复

恢复方法1:进程终止

逐个终止线程,直到死锁消除。

◆终止顺序:

1.优先级(是前台交互还是后台处理)

2.已占用资源、还需要的资源

3.已经运行时间

恢复方法2:资源抢占

◆把已经分发出去的锁给收回来

◆让线程回退几步,这样就不用结束整个线程,成本比较低

◆缺点:可能同一个线程一直被抢占,那就造成饥饿

6. 实际工程中如何避免死锁

  1. 设置超时时间

    • Lock的tryLock(long timeout, TimeUnit unit)
    • synchronized不具备尝试锁的能力
    • 造成超时的可能性多: 发生了死锁、线程陷入死循环、线程 执行很慢
    • 获取锁失败:打日志、发报警邮件、重启等
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
​
/*** 描述:     用tryLock来避免死锁*/
public class TryLockDeadlock implements Runnable {​int flag = 1;static Lock lock1 = new ReentrantLock();static Lock lock2 = new ReentrantLock();
​public static void main(String[] args) {TryLockDeadlock r1 = new TryLockDeadlock();TryLockDeadlock r2 = new TryLockDeadlock();r1.flag = 1;r2.flag = 0;new Thread(r1).start();new Thread(r2).start();}
​@Overridepublic void run() {for (int i = 0; i < 100; i++) {if (flag == 1) {try {if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {System.out.println("线程1获取到了锁1");Thread.sleep(new Random().nextInt(1000));if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {System.out.println("线程1获取到了锁2");System.out.println("线程1成功获取到了两把锁");lock2.unlock();lock1.unlock();break;} else {System.out.println("线程1尝试获取锁2失败,已重试");lock1.unlock();Thread.sleep(new Random().nextInt(1000));}} else {System.out.println("线程1获取锁1失败,已重试");}} catch (InterruptedException e) {e.printStackTrace();}}if (flag == 0) {try {if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {System.out.println("线程2获取到了锁2");
​Thread.sleep(new Random().nextInt(1000));if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {System.out.println("线程2获取到了锁1");System.out.println("线程2成功获取到了两把锁");lock1.unlock();lock2.unlock();break;} else {System.out.println("线程2尝试获取锁1失败,已重试");lock2.unlock();Thread.sleep(new Random().nextInt(1000));}} else {System.out.println("线程2获取锁2失败,已重试");}} catch (InterruptedException e) {e.printStackTrace();}}}}
}

2.多使用并发类而不是自己设计锁

◆ConcurrentHashMapConcurrentLinkedQueue、 AtomicBoolean等

◆实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高

◆多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好

◆并发场景需要用到map ,首先想到用ConcurrentHashMap

3.尽量降低锁的使用粒度 :用不同的锁而不是一个锁

4.如果能使用同步代码块,就不使用同步方法:自己指定锁对象

5.给你的线程起个有意义的名字: debug和排查时事半功倍,框架 和JDK都遵守这个最佳实践

6.避免锁的嵌套: MustDeadLock类

7.分配资源前先看能不能收回来:银行家算法

8.尽量不要几个功能用同一把锁:专锁专用

7. 其他活性故障(又叫活跃性问题)

◆死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题

◆活锁( LiveLock )

有死锁和资源耗尽的风险

死锁:每个哲学家都拿着左手的餐叉,永远都在等右边的餐 叉(或者相反) 活锁:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉, 那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再 等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中, 缺乏餐叉可以类比为缺乏共享资源

7.1 活锁

◆什么是活锁

  • 虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事
  • 如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待
  • 如果发生活锁,那么这里的情况就是,双方都不停地对对方说 “你先起来吧,你先起来吧”, 双方都一直在说话,在运行
  • 死锁和活锁的结果是一样的,就是谁都不能先抬头

◆代码演示

import java.util.Random;
​
/*** 描述:     演示活锁问题*/
public class LiveLock {//spoon为勺子static class Spoon {//勺子的拥有者private Diner owner;
​public Spoon(Diner owner) {this.owner = owner;}
​public Diner getOwner() {return owner;}
​public void setOwner(Diner owner) {this.owner = owner;}
​public synchronized void use() {System.out.printf("%s吃完了!", owner.name);
​
​}}//Diner为就餐者static class Diner {​private String name;private boolean isHungry;
​public Diner(String name) {this.name = name;isHungry = true;}
​public void eatWith(Spoon spoon, Diner spouse) {while (isHungry) {if (spoon.owner != this) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}continue;}
​if (spouse.isHungry) {System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");spoon.setOwner(spouse);continue;}
​spoon.use();isHungry = false;System.out.println(name + ": 我吃完了");spoon.setOwner(spouse);
​}}}
​
​public static void main(String[] args) {Diner husband = new Diner("牛郎");Diner wife = new Diner("织女");
​Spoon spoon = new Spoon(husband);
​new Thread(new Runnable() {@Overridepublic void run() {husband.eatWith(spoon, wife);}}).start();
​new Thread(new Runnable() {@Overridepublic void run() {wife.eatWith(spoon, husband);}}).start();}
}
​

原因:重试机制不变,消息队列始终重试,吃饭始终谦让

如何解决活锁问题:

  • 以太网的指数退避算法
  • 加入随机因素
        public void eatWith(Spoon spoon, Diner spouse) {while (isHungry) {if (spoon.owner != this) {try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}continue;}//加入随机Random random = new Random();if (spouse.isHungry && random.nextInt(10) < 9) {System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");spoon.setOwner(spouse);continue;}
​spoon.use();isHungry = false;System.out.println(name + ": 我吃完了");spoon.setOwner(spouse);
​}}}

◆工程中的活锁实例:消息队列

  • 策略:消息如果处理失败,就放在队列开头重试
  • 由于依赖服务出了问题,处理该消息一直失败.
  • 没阻塞,但程序无法继续
  • 解决:放到队列尾部、重试限制

7.2 饥饿

8. 面试常考问题

Java并发核心知识体系精讲---死锁的前世今生相关推荐

  1. 线程八大核心+java并发核心知识体系精讲_Java从业者如果不懂这些,面试80%都会挂在这些核心知识上面...

    JVM 无论什么级别的Java从业者,JVM都是进阶时必须迈过的坎.不管是工作还是面试中,JVM都是必考题.如果不懂JVM的话,薪酬会非常吃亏(近70%的面试者挂在JVM上了) 详细介绍了JVM有关于 ...

  2. java并发核心知识体系精讲_JAVA核心知识汇总

    双非同学如何逆袭大厂? 在互联网行业,入行的第一份工作很大程度上决定了以后职业发展的高度.有些双非的同学认为自己校招进不了大厂以后还会有社招,这种想法很危险.大厂的社招,大多数都只招大厂的员工.什么意 ...

  3. java并发核心知识体系精讲_JVM核心知识体系

    作者:林振华 来源:编程原理 1.问题 如何理解类文件结构布局? 如何应用类加载器的工作原理进行将应用辗转腾挪? 热部署与热替换有何区别,如何隔离类冲突? JVM如何管理内存,有何内存淘汰机制? JV ...

  4. java并发核心知识体系精讲_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析

    版权说明:本文内容根据 github 开源项目整理所得 项目地址: https://github.com/Snailclimb/JavaGuide​github.com 一.基础 什么是线程和进程? ...

  5. Java多线程,并发核心知识体系总结

    目录 Java底层原理: 线程终止--interrupt 线程安全--运算结果出错, 活跃性问题:死锁.活锁.饥饿锁, 对象发布和初始化的线程安全 Java三兄弟--JVM内存结构,Java内存模型, ...

  6. 40000+字超强总结?阿里P8把Java全栈知识体系详解整理成这份PDF

    40000 +字长文总结,已将此文整理成PDF文档了,需要的见文后下载获取方式. 全栈知识体系总览 Java入门与进阶面向对象与Java基础 Java 基础 - 面向对象 Java 基础 - 知识点 ...

  7. 视频教程-Java进阶高手课-Spring精讲精练-Java

    [ [这里是图片001] Java进阶高手课-Spring精讲精练 中国科学技术大学硕士研究生,丹麦奥尔堡大学访问学者,先后就职于eBay.蚂蚁金服.SAP等国内外一线互联网公司,在Java后端开发. ...

  8. Java架构师知识体系汇总

    Java架构师知识体系汇总 源码分析 常用设计模式 Proxy代理模式 Factory工厂模式 Singleton单例模式 Delegate委派模式 Strategy策略模式 Prototype原型模 ...

  9. 1. JAVA全栈知识体系--- Java基础

    1. JAVA全栈知识体系- Java基础 文章目录 1. JAVA全栈知识体系--- Java基础 1.1 语法基础 面向对象特性? a = a + b 与 a += b 的区别 3*0.1 == ...

最新文章

  1. ATG中的定时Job处理
  2. 使用ORM Profiler分析数据访问性能
  3. 根据excel定义的表机构,导入powerdesigner
  4. (56)zabbix Screens视图配置
  5. luogu 4768
  6. 【转载】会议是浪费工作时间的最佳去处
  7. 源码分析Dubbo服务消费端启动流程
  8. eclipse maven项目 class类部署不到tomcat下_Springboot介绍以及用Eclipse搭建一个简单的Springboot项目教程
  9. 自学前端一般几年可以精通,找个差不多的工作?
  10. [转]服务器监控 UptimeRobot 简明使用手册
  11. 神州数码c语言笔试题,神州数码应聘笔试题(2)
  12. WPF 自定义文本框输入法 IME 跟随光标
  13. 拼多多和酷家乐面试经历总结(已拿offer)
  14. 阿里巴巴大数据技术专家岗面试题
  15. ubuntu 22.04 网易云音乐安装
  16. 基于python的图像识别
  17. vue单页面应用项目优化总结
  18. Rockchip RK3588 kernel dts解析之显示模块
  19. Easyrecovery教你Excel表格数据恢复
  20. ElasticSearch-查询语法(结构化查询)

热门文章

  1. 微型计算机CLD什么意思,房地产里说的CLD是什么意思
  2. Linux 学习1
  3. excel小写转大写公式_谁说WPS干不过Excel?这几个功能WPS更强
  4. 企信通 短信接口对接
  5. [含论文+源码等]基于JavaEE的酒店点餐收款系统S2SH
  6. equb在线转PDF(亲测可用,速度快)
  7. Quantum Espresso + Phonopy 计算声子过程
  8. 用Orachard与Bootstrap建站
  9. Linux下常用软件压缩包方式安装
  10. 如何搭建本地的yum源(CeontOS7)?