Java多线程之死锁问题,wait和notify

news2024/9/23 19:18:55

文章目录

  • 一. synchronnized 的特性
    • 1. 互斥性
    • 2. 可重入性
  • 二. 死锁问题
    • 1. 什么是死锁
    • 2. 死锁的四个必要条件
    • 3. 常见的死锁场景及解决
      • 3.1 不可重入造成的死锁
      • 3.2 循环等待的场景
        • 哲学家就餐问题(多个线程多把锁)
        • 两个线程两把锁
  • 三. Object类中提供线程等待的方法
    • 1. 常用方法
    • 2. wait和notify的搭配使用
    • 3. wait 和 sleep 的区别
    • 4. 练习: 顺序打印ABC

这个博客研究的死锁问题是基于Java当中进行叙述的, 而在Java当中, 与死锁问题息息相关的就是 synchronized关键字了.

一. synchronnized 的特性

1. 互斥性

synchronized 会起到互斥效果, 这里的互斥其实很好理解, 一个线程执行到某个对象的 synchronized 中时, 此时就是针对这个对象加锁了, 而如果此时其他线程如果也想要使用 synchronized 针对同一个对象进行加锁, 就必须等到该对象对象上的锁释放掉才行, 这便是互斥的效果了.

2. 可重入性

同一个线程针对同一个对象, 连续加锁两次, 是否会有问题; 如果没问题, 就是可重入的, 如果有问题, 就是不可重入的.

看下面的代码, 在Java当中是可行的.

class Counter {
    public int count = 0;

    synchronized public void add() {
        synchronized (this) {
            count++;
        }
    }
}

这里的锁对象是this只要有线程调用add, 进入add方法的时候,就会先加锁(能够加锁成功), 紧接着又遇到了代码块, 再次尝试加锁.

站在this的视角(锁对象)它认为自己已经被另外的线程给占用了, 这里的第二次加锁是否要阻塞等待呢? 如果这里的第二次获取锁成功, 这个锁就是可重入的, 如果进入阻塞等待的状态, 就是不可重入的, 此时如果进入了阻塞等待大的状态, 可想而知, 我们的程序就 “僵住了” , 这也就是是一种死锁的情况了.

上面的代码在Java代码中是很容易出现的, 为了避免上面所说情况的出现, Java中 synchronized 就被设置成可重入的了.

synchronized可重入的特性其实就是是在锁对象里面记录一下, 当前的锁是哪个线程持有的, 如果再次加锁的线程和持有线程是同一个, 就可以获取锁, 否则就阻塞等待.

二. 死锁问题

1. 什么是死锁

死锁是指两个或两个以上的进程在执行过程中, 由于竞争资源或者由于彼此通信而造成的一种阻塞的现象, 若无外力作用, 它们都将无法推进下去; 此时称系统处于死锁状态或系统产生了死锁, 这些永远在互相等待的进程称为死锁进程; 通俗点说, 死锁就是两个或者多个相互竞争资源的线程, 你等我, 我等你, 你不放我也不放, 这就造成了他们之间的互相等待, 导致了 “永久” 阻塞.

一旦程序出现死锁, 就会导致线程无法继续执行后续的工作, 程序势必会有严重的bug, 而且是死锁非常隐蔽的, 开发阶段, 不经意间, 就会写出死锁代码, 还不容易测试出来, 所以这就需要我们对死锁问题有一定的认识以方便我们以后的调试和修改.

2. 死锁的四个必要条件

  1. 互斥使用: 线程1拿到了锁, 线程2就得进入阻塞状态(锁的基本特性).
  2. 不可抢占: 线程1拿到锁之后, 必须是线程1主动释放, 不可能线程1还没有释放, 线程2强行获取到锁.
  3. 请求和保持: 线程1拿到锁A后, 再去获取锁B的时候, A这把锁仍然保持, 不会因为要获取锁B就把A释放了.
  4. 循环等待: 线程1先获取锁A再获取锁B, 线程2先获取锁B再获取锁A, 线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A.

而在Java代码中, 前三点 synchronized锁的基本特性, 我们是无法改变的, 循环等待是这四个条件里唯一 一个和代码结构相关的, 是我们可以控制的.

3. 常见的死锁场景及解决

3.1 不可重入造成的死锁

同一个线程针对同一个对象, 连续加锁两次, 如果锁不是可重入锁, 就会造成死锁问题.

最开始介绍synchronized的特性的时候所说, synchronized具有可重入性, 而在Java中还有一个ReentrantLock锁也是可重入锁, 所以说, 在Java程序中, 不会出现这种死锁问题.

3.2 循环等待的场景

哲学家就餐问题(多个线程多把锁)

  • 场景

img

有五位沉默的哲学家围坐在一张圆桌旁, 每个哲学家有两种状态.

  1. 思考人生(相当于线程的阻塞状态)
  2. 拿起筷子吃面条(相当于线程获取到锁然后执行一些计算)

有五只筷子供他们使用, 哲学家需要拿到左手和右手边的两根筷子之后才能吃饭, 吃完后将筷子放下继续思考.

由于操作系统随机调度, 这五个哲学家, 随时都可能想吃面条, 也随时可能要思考人生.

假设出现了极端情况, 同─时刻, 所有的哲学家同时拿起右手的筷子, 哲学家们需要再拿起左手的筷子才可以吃面条, 而此时他们发现没有筷子可以拿了, 都在等左边的哲学家放下筷子, 这里的筷子落实到程序中就相当于锁, 此时就陷入了互相阻塞等待的状态, 这种场景就是典型的因为循环等待造成的死锁问题.

img

  • 解决方案

我们可以给按筷子编号, 哲学家们拿筷子时需要遵守一个规则, 拿筷子需要先拿编号小的, 再拿编号大的, 再来看这个场景, 哲学家 2, 3, 4, 5 分别拿起了两手边编号为 1, 2, 3, 4 编号较小的筷子, 而1号哲学家想要拿到编号编号较小的1号筷子发现已经被拿走了, 此时就空出了5号筷子, 这样5号哲学家就可以拿起5号筷子去吃面条了, 等5号哲学家放下筷子后, 4号哲学家就可以拿起4号筷子去吃面条了, 以此类推…

img

对应到程序中, 这样的做法其实就是在给锁编号, 然后再按照一个规定好的顺序来加锁, 任意线程加多把锁的时候, 都让线程遵守这个顺序, 这样就解决了互相阻塞等待的问题.

两个线程两把锁

两个线程两把锁, t1, t2线程先各自针对锁A, 锁B加锁, 然后再去获取对方的锁, 此时双方就会陷入僵持状态, 造成了死锁问题.

img

这里可以看一下这里举出来的现实中的例子来理解这里的场景:

前段时间疫情还没有放开的时候, 走到哪里都离不开健康码, 某一天这个健康码就给给崩了, 手机上的健康码没办法正常打开了, 于是程序员就赶到公司去修复这个bug, 但是在公司楼下被保安拦住了, 保安要求出示健康码才能上楼, 程序员说: “健康码出问题了, 我上楼修复了才能出示健康码” ; 保安又说: “你出示了健康码才能上楼”; 此时场景就陷入了僵持的状态, 程序员上不了楼, 健康码也无法修复; 这个场景就可以类比这里的锁问题.

观察下面的代码及执行结果:

这里的代码是为了构造一个死锁的场景, 代码中的sleep是为了确保两个线程先把第一个锁拿到, 因为线程是抢占式执行的, 如果没有sleep的作用, 这里的死锁场景是不容易构造出来的.

public class TestDemo14 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (B) {
                System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (A) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

执行结果:

img

看这里的执行结果, t1线程获取到了锁A但并没有获取到锁B, t2线程获取到了锁B但并没有获取到锁A, 也就是说t1t2两个线程进入了相互阻塞的状态, 线程无法获去到两把锁, 我们可以使用jconsole工具来观察一下这两个线程的状态, 分析一下是哪里的代码造成这里死锁问题的.

可以发现, t1线程此时是处于BLOCKED状态的, 表示获取锁, 获取不到的阻塞状态; 根据堆栈跟踪的信息反映在代码中是在第14行.

img

同样的, t2线程此时也是处于BLOCKED阻塞状态的; 根据堆栈跟踪的信息反映在代码中是在第27行.

img

上面叙述的是两个线程死锁问题的代码场景和具体分析, 那么这里的锁问题如何解决呢?

其实也不需要特别复杂的算法, 实际开发中只需要解单高效的解决问题即可, 复杂了反而会使程序容易出bug, 可能会引出新的问题, 就比如上面介绍的哲学家就餐问题通过限制加锁顺序来解决死锁问题就是一种简单高效的解决办法, 而这里也一样, 也可以通过控制加锁的顺序来解决, 我们让t1t2两个线程都按照相同的顺序来获取锁, 比如这里规定先获取锁A, 再获取锁B, 这样按照相同的顺序去获取锁就避免了循环等待造成的死锁问题, 代码如下:

public class TestDemo14 {
    public static void main(String[] args) {
        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                }
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            synchronized (A) {
                System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (B) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

最后的执行结果两个线程都获取到了A,B锁.

img

三. Object类中提供线程等待的方法

1. 常用方法

除了Thread类中的能够实现线程等待的方法, 如join, sleep, 在Object类中也提供了相关线程等待的方法.

方法解释
public final void wait() throws InterruptedException释放锁并使线程进入WAITING状态
public final native void wait(long timeout) throws InterruptedException相比于上面, 多了一个最长等待时间
public final void wait(long timeout, int nanos) throws InterruptedException等待的最长时间精度更大
public final native void notify();随机唤醒一个WAITING状态的线程, 并加锁, 搭配wait方法使用
public final native void notifyAll();唤醒所有处于WAITING状态的线程, 并加锁(很可能产生锁竞争), 搭配wait方法使用

我们知道由于线程之间的抢占式执行和操作系统的随机调度会导致线程之间执行顺序是 “随机” 的, 但在实际开发中很多场景下我们是希望可以协调多个线程之间的执行先后顺序的.

虽然线程在内核里的调度是随机的, 这个我们是没办法改变的, 但是我们可以通过一些api让线程主动阻塞, 主动放弃CPU来给别的线程让路, 以此来控制线程之间的执行顺序.

Thread类中的joinsleep方法定程度上也能控制线程的执行顺序, 但通过join和sleep控制并不够灵活:

  1. 使用join, 则必须要t1彻底执行完, t2才能执行; 如果是希望t1先干50%的活, 就让t2开始行动, join就无能为力了.
  2. 使用sleep, 指定一个休眠时间的, 但是t1执行的这些任务, 到底花了多少时间, 是不好估计的.

而使用waitnotify可以更好的解决上述的问题.

下面的代码t线程中没有使用synchronized进行加锁, 直接调用了wait方法, 会产生非法锁状态异常.

public class TestDemo15 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行完毕!");
        });

        t.start();
        System.out.println("wait前");
        t.wait();
        System.out.println("wait后");
    }
}

执行结果:
img

之所以这里会抛出这个异常, 是因为wait方法的执行步骤为:

  1. 先释放锁
  2. 再让线程阻塞等待
  3. 最后满足条件后, 重新尝试获取锁, 并在获取到锁后, 继续往下执行

而上面的代码都没有加锁, 又怎么能释放锁锁呢, 所以会抛出异常, 所以说, wait操作需要搭配synchronized来使用.

所以对上面的代码做出如下修改即可,

synchronized (t) {
    System.out.println("wait前");
    t.wait();
    System.out.println("wait后");
}

执行结果:

img

2. wait和notify的搭配使用

wait方法常常搭配notify方法搭配一起使用, notify方法用来唤醒wait等待的线程, wait能够释放锁, 使线程等待, 而notify唤醒线程后能够获取锁, 然后使线程继续执行, 执行流程如下:

img

在Java中, notify方法也需要在加锁前提下使用.

代码示例:

public class TestDemo16 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();

        Thread t1 = new Thread(() -> {
            // 这个线程负责进行等待
            System.out.println("t1: wait 之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1: wait 之后");
        });

        Thread t2 = new Thread(() -> {
            System.out.println("t2: notify 之前");
            synchronized (object) {
                // notify 务必要获取到锁, 才能进行通知
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                object.notify();
            }
            System.out.println("t2: notify 之后");
        });

        t1.start();
        // 此处写的 sleep 500 是大概率会让当前的 t1 先执行 wait 的.
        // 极端情况下 (电脑特别卡的时候), 可能线程的调度时间就超过了 500 ms
        // 还是可能 t2 先执行 notify.
        Thread.sleep(500);
        t2.start();
    }
}

执行结果:

img

注意事项:

  1. 虽然这里wait是阻塞了, 阻塞在synchronized代码块里, 实际上, 这里的阻塞是释放了锁的, 此时其他线程是可以获取到object这个对象的锁的, 这里的阻塞,就处在WAITING状态.

img

  1. 代码中的锁对象和调用wait, notify方法的对象必须是相同的才能够起到应有的效果, notify只能唤醒在同一个对象上等待的线程.

img

  1. 代码中要保证先执行wait, 后执行notify才是有意义的.

img

  1. wait无参数版本, 是一个死等的版本, 只要不进行notify, 就会死等下去, 可以采用wait带参数版本设计代码避免死等可能出现的问题.

3. wait 和 sleep 的区别

  • 相同点
  1. 都可以使线程暂停一段时间来控制线程之间的执行顺序.
  2. wait可以设置一个最长等待时间, 和sleep一样都可以提前唤醒.
  • 不同点
  1. wait是Object类中的一个方法, sleep是Thread类中的一个方法.
  2. wait必须在synchronized修饰的代码块或方法中使用, sleep方法可以在任何位置使用.
  3. wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作.
  4. 使用sleep只能指定一个固定的休眠时间, 线程中执行操作的执行时间是无法确定的; 而使用wait在指定操作位置就可以唤醒线程.
  5. sleep和wait都可以被提前唤醒, interruppt唤醒sleep, 是会报异常的, 这种方式是一个非正常的执行逻辑; 而noitify唤醒wait是正常的业务执行逻辑, 不会有任何异常.

4. 练习: 顺序打印ABC

有三个线程, 分别只能打印A, B, C, 实现代码控制三个线程固定按照ABC的顺序打印.

public class TestdDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            System.out.println("A");
            synchronized (locker1) {
                locker1.notify();
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("B");

            synchronized (locker2) {
                locker2.notify();
            }
        });
        
        Thread t3 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("C");
        });

        t2.start();
        t3.start();
        Thread.sleep(100);
        t1.start();
    }
}

执行结果:

img

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/137835.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

剑指offer----C语言版----第八天

目录 1. 矩阵中的路径 1.1 题目描述 1.2 基础知识 1.3 思路分析 1.4 小试牛刀 1. 矩阵中的路径 原题链接: 剑指 Offer 12. 矩阵中的路径 - 力扣(LeetCode)https://leetcode.cn/problems/ju-zhen-zhong-de-lu-jing-lcof/submissions/ 1.1 题…

c++11 标准模板(STL)(std::deque)(七)

定义于头文件 <deque> std::deque 修改器 清除内容 std::deque<T,Allocator>::clear void clear(); (C11 前) void clear() noexcept; (C11 起)从容器擦除所有元素。此调用后 size() 返回零。 非法化任何指代所含元素的引用、指针或迭代器。任何尾后迭代器亦被…

静态主机清单

自定义环境 1.操作环境 需要保准三台主机能够互相通信。设置同一种网络模式&#xff08;nat&#xff09;。 2.配置IP地址 server---192.168.90.134 node1---192.168.90.135 node2---192.168.90.133 3.修改主机名 hostnamectl set-hostname server.exmaple.com hostnamec…

【安全硬件】Chap.3 如何插入一个硬件木马到芯片的组合逻辑电路的漏洞里?不影响正常电路的功能的情况下进行硬件的逻辑加密

【安全硬件】Chap.3 如何插入一个硬件木马到芯片的组合逻辑电路的漏洞里&#xff1f;如何进行硬件的逻辑加密在不影响正常电路的功能的情况下1. 组合逻辑电路的漏洞组合逻辑电路中的硬件木马—举例Fault injection attacks士兵巡逻预案系统解决方法——硬件的逻辑加密&#xff…

坦克大战2.0,3.0,4.0版本

1.坦克大战 0.3 在坦克大战游戏(0.2版)基础上添加如下功能:当玩家按一下j键,就发射一颗子弹. 编写Shot类 package com.yt.tankgame03;/*** 射击子弹*/ public class Shot implements Runnable{int x;//子弹的x坐标int y;//子弹的y坐标int direct;//子弹的方向int speed 2;//子…

Python 做了一个 ChatGPT 机器人,表妹玩了一下午

大家好&#xff0c;我是毕加锁。 毫无疑问&#xff0c;ChatGPT 已经是当下编程圈最火的话题之一&#xff0c;它不仅能够回答各类问题&#xff0c;甚至还能执行代码&#xff01; 或者是变成一只猫 因为它实在是太好玩&#xff0c;我使用Python将ChatGPT改造&#xff0c;可以实现…

【回答问题】ChatGPT上线了!给我推荐20个比较流行的知识蒸馏模型/方法/源码

目录给我推荐20个比较流行的知识蒸馏模型给我推荐10个比较流行的知识蒸馏模型源码给我推荐20个比较流行的深度学习知识蒸馏方法给我推荐20个比较流行的深度学习知识蒸馏方法源码给我推荐20个比较流行的深度学习知识蒸馏实现源码给我推荐20个比较流行的知识蒸馏模型 好的&#…

VSCode配置并连接远程服务器 并设置免密登录

文章目录1 前言PyCharm与VSCode2 VSCode配置远程开发环境3 VSCode配置远程免密登录4 推荐插件参考1 前言 PyCharm与VSCode 最近由于许多深度学习的项目需要在服务器上跑&#xff0c;之前一直使用PyCharm进行开发&#xff0c;使用习惯之后觉得还行&#xff0c;配置稍微有点复杂…

(七)Docker上安装常用软件说明(tomcat,mysql)

目录 一、总体步骤 二、安装tomcat 三、安装mysql&#xff08;简易版&#xff09; 四、安装mysql&#xff08;实战版&#xff09; 一、总体步骤 1、搜索镜像 2、拉取镜像 3、查看镜像 4、启动镜像 5、停止容器 6、移除容器 二、安装tomcat 1、dockerhub官网搜索tom…

福利来了~Python内置函数最全总结,建议收藏

喜迎2023&#xff0c;希望所有人都幸福安康&#xff01;小编在这里给大家放烟花了&#xff0c;希望新的一年疫情嗝屁&#xff0c;霉运全跑&#xff01; 开始正文。 1 abs() 绝对值或复数的模 In [1]: abs(-6) Out[1]: 6 2 all() 接受一个迭代器&#xff0c;如果迭代器的所有…

前端bug每次都比后端多,我总结了5点原因

最近总有朋友跟我吐槽说&#xff0c;每次一汇报&#xff0c;就说前端bug多&#xff0c;前端能力有问题&#xff0c;几乎每次都是前端bug比后端多&#xff0c;还好几次导致项目延期。 我其实听的挺不是滋味的&#xff0c;前端bug比后端多&#xff0c;可能是这么几点原因吧 目录 …

Shiro历史漏洞复现 - Shiro-550

文章目录Shiro简介Shiro历史漏洞Shiro-550Shiro-721漏洞发现Shiro组件识别Shiro漏洞搜索Shiro漏洞检测工具Shiro rememberMe反序列化漏洞&#xff08;Shiro-550&#xff09;漏洞原理影响版本漏洞利用Shiro-721 (未完待续......&#xff09;Shiro简介 Apache Shiro是一种功能强…

LeetCode刷题复盘笔记—一文搞懂动态规划之115. 不同的子序列问题(动态规划系列第三十九篇)

今日主要总结一下动态规划的一道题目&#xff0c;115. 不同的子序列 题目&#xff1a;115. 不同的子序列 Leetcode题目地址 题目描述&#xff1a; 给定一个字符串 s 和一个字符串 t &#xff0c;计算在 s 的子序列中 t 出现的个数。 字符串的一个 子序列 是指&#xff0c;通…

spring boot 日志

目录 什么是日志呢 ? 日志有什么作用呢? 什么是日志呢 ? 日志的作用是什么呢 ? 我们需要学会日志的什么 ? 自定义输出日志 日志持久化 为什么要将日志持久化呢? 如何进行日志的持久化 设置日志级别 日志级别有什么用呢 ? 日志都有哪些级别呢 ? 如何设置日志…

十二、Express接口编写 —— 跨域问题

在前面的HTTP模块内容内容当中讲到这个跨域的问题&#xff0c;跨域就涉及到浏览器的同源策略&#xff0c;跨域只出现在浏览器当中&#xff0c;在浏览器当中去执行脚本的时候会进行一个同源检测&#xff0c;只有是同源的脚本才会被浏览器执行&#xff0c;不同源就是跨域&#xf…

MySQL高级 SQL优化【order bygroup by优化】

目录 1&#xff1a;SQL优化 1.1&#xff1a;order by优化 A. 数据准备 B. 执行排序SQL C. 创建索引 D. 创建索引后&#xff0c;根据age, phone进行升序排序 E. 创建索引后&#xff0c;根据age, phone进行降序排序 F. 根据phone&#xff0c;age进行升序排序&#xff…

STM32MP157驱动开发——Linux 网络设备驱动

STM32MP157驱动开发——Linux 网络设备驱动一、简介STM32MP1 GMAC 接口简介YT8511C 详解二、驱动开发1.网络外设的设备树2.设备驱动三、测试网速测试参考文章&#xff1a;【正点原子】I.MX6U嵌入式Linux驱动开发——Linux 网络驱动 一、简介 网络驱动是 linux 驱动三巨头之一&…

[C语言]三种方法实现n的k次方(递归/调用math库函数/实现pow函数)[含递归图解说明]

目录 1.调用math库函数中的pow函数实现n的k次方 2.创造pow函数实现n的k次方 3.递归实现n的k次方&#xff08;含图解&#xff09; 1.调用math库函数中的pow函数实现n的k次方 pow函数的功能&#xff1a;计算n的k次幂 pow格式&#xff1a;pow(n,k) #include <stdio.h>#in…

@NotEmpty、@NotBlank、@NotNull 区别和使用

这种注解通常使用场景在前端发送过来的数据&#xff0c;先进行校验处理&#xff0c;在进行逻辑判断的&#xff0c;所以在进行校验处理的时候&#xff0c;我们通常会使用这三种注解来进行判断传过来的值 1NotNull 适用于基本数据类型(Integer&#xff0c;Long&#xff0c;Doubl…

回收租赁商城系统功能拆解01讲-产品架构

回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 可以快速帮助企业搭建类似闲鱼回收/爱回收/爱租机/人人租等回收租赁商城。 回收租赁系统支持智能评估回收价格&#xff0c;后台调整最终回收价&#xff0c;用户同意回收后系统即刻放款&#xff0c;用户微信零…