Java Web 实战 03 - 多线程基础(2)

news/2024/11/28 11:53:47/

Java Web 实战 03 - 多线程基础篇 2

  • 二 . Thread类常见方法
    • 2.1 Thread 的常见构造方法
    • 2.2 Thread 的几个常见属性
      • getId()
      • getName()
      • getState()
      • getPriority()
      • isDaemon()
      • 案例 : 实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()
    • 2.3 启动一个线程-start()
    • 2.4 中断线程(线程执行结束)
      • 2.4.1 手动创建的标志位
      • 2.4.2 Thread自带的标志位
    • 2.5 线程等待 join
      • 2.5.1 join 是怎么回事?
      • 2.5.2 栗子1 : 让 main 等待 t2 , t2 去等待 t1
      • 2.5.3 栗子2 : 控制 main 先运行 t1 , t1 执行完再运行 t2
      • 2.5.4 带参数版本的 join
    • 2.6 获取到线程引用 : Thread.currentThread()
    • 2.7 线程的休眠 : sleep
  • 三 . 线程的状态
    • 3.1 通过代码来看线程的状态
    • 3.2 线程状态和状态转移的意义

大家好 , 这篇文章给大家带来的是多线程相关的基础知识 , 我们会介绍一下Thread 类常见的方法都有什么 , 以及启动线程、中断线程、线程等待、获取线程引用、现成的休眠等问题 , 然后再给大家介绍一下线程的状态都有什么这几个问题。
上一篇文章的链接在这里 , 大家移步观看
http://t.csdn.cn/JVErX
由于 C 站的编辑器不太好用 , 导致许多排版没能生效 , 大家可移步至这里观看

https://www.yuque.com/jialebihaitao/study/qzym2pw332lm6k7q?singleDoc# 《2. 多线程 (基础)》

感谢大家的支持~

二 . Thread类常见方法

2.1 Thread 的常见构造方法

方法说明
Thread()创建线程对象 (已经用过了)
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名

其中第一个和第二个我们已经使用过

  1. Thread() : 自己创建线程的子类 , 再去 new 他的实例
Thread t = new MyThread();
  1. Thread(Runnable target) : 使用 Runnable 创建一个任务 , 然后将 Runnable 的任务放到线程中 , 这样也可以创建一个线程
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
  1. Thread(String name) : 这个操作是给线程起名字,我们之前看到的 Thread-0 就是 Java 帮我们命名的,线程在操作系统里面是没有名字的,Java 为了帮助程序员便于理解,就在 JVM 里面给对应的 Thread 对象起了个名,这个线程是和内核中的线程一一对应的 , 这个名字对于程序的执行 , 没有任何影响 , 对于程序员来说调试挺有用的
Thread t3 = new Thread("这是我的名字");
  1. Thread(Runnable target, String name) : 既实现了命名 , 又实现了使用 Runnable 对象创建线程对象
public class Demo7 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while(true) {System.out.println("Hello MyThread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}},"俺的线程");t.start();while(true) {System.out.println("Hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

我们使用 jconsole 工具查看当前线程 , 就发现有我们自己重新命名的线程了
image.png

2.2 Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isinterrupted()

getId()

获取到的是 线程 在 JVM 里面的身份标识

线程里面的身份标识是有好几个的

  1. 内核的 PCB 上有标识
  2. 用户态线程库里面也有标识 (pthread , 操作系统提供的线程库)
  3. JVM里面也有标识 (JVM 的 Thread 类底层也是调用操作系统的 pThread 库)

这三个标识各不相同,但是目的都是作为身份的区分,我们可以理解为身份标识的各种小名

getName()

获取在Thread的构造方法里面,传入的姓名 (刚才在 Thread 构造方法中传入的名字)

getState()

获取到 PCB 里面的状态 , 这个状态表示线程当前所处的一个情况 . 此处得到的状态是 JVM 里面设立的状态体系 , 这个状态比操作系统内置的状态要更丰富

getPriority()

获取到线程的优先级 , 优先级高的线程理论上来说更容易被调度到

isDaemon()

判断是不是后台线程
Daemon的意思是 “守护线程” 的意思 , 我们可以理解为 “后台线程”
线程分为"前台线程"与"后台线程" , 我们可以通过类比手机的正在运行应用 (前台应用) 与后台应用来理解前台线程与后台线程
一个线程 , 创建出来默认就是前台线程 , 前台线程会阻止进程结束 , 进程会保证所有的前台线程都执行完毕 , 才会退出 . main 线程就是一个前台线程
如果我们屏蔽了 main 线程 , 正常情况下线程就会退出
但是运行并非如此
image.png
这是因为我们上面新建的线程默认就是前台线程 , 我们需要将自己创建的线程修改成后台线程

public class Demo7 {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while(true) {System.out.println("Hello MyThread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}},"俺的线程");t.setDaemon(true);// 设置成后台线程t.start();// while(true) {//     System.out.println("Hello main");//     try {//         Thread.sleep(1000);//     } catch (InterruptedException e) {//         e.printStackTrace();//     }// }}
}

这样我们的线程就会嗖的一下执行完
image.png
有个小问题 :
❗ 我们发现 , 怎么运行了多次 , 怎么有的时候打印有的时候就不打印呢?
image.png
✅ 举个栗子 : 当我们刚打完一局游戏 , 想要打开 B 站去学习 , 这时候点开了 B 站 , B 站这个时候就是"前台应用" , 必须是在执行的 , 不能被销毁

前台线程 : 前台线程会阻止进程结束 , 一个线程创建出来默认就是前台线程 , 进程会保证所有前台进程执行完毕才结束程序

后台的游戏就被隐藏到任务栏里面了 , 其实还运不运行已经无所谓了 , 因为我们已经玩完了

后台线程 : 后台线程不会阻止进程结束 , 比如 : main线程执行结束 , 整个线程就结束了

这是因为我们把 t 线程设置成了后台线程 , 当 main线程执行完毕整个程序就执行完了 , 我们这里并未对 main 线程做任何操作 , 所以有可能是在 main 线程结束之前运行了一下 t 线程 , 也有可能是直接就执行结束了 , t 线程还没来得及执行


案例 : 实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()

接下来 , 我们写一个方法 , 来实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()

public class Demo8 {public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("Hello MyThread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}},"俺的线程");System.out.println(t.getId());// 获取线程IDSystem.out.println(t.getName());// 获取线程名称System.out.println(t.getState());// 获取线程状态System.out.println(t.getPriority());// 获取线程优先级System.out.println(t.isDaemon());// 获取是否是后台线程System.out.println(t.isAlive());// 获取线程是否存活}
}

image.png

2.3 启动一个线程-start()

我们之前已经使用过这个方法了
给大家强调一点 :
(安排任务)创建 Thread 实例 , 并没有真正的在操作系统内核中创建出线程 . 这里的任务是通过 Thread 的 run 或者 Runnable 或者 lambda 来体现具体的任务内容

run 方法只是描述任务 , 这一点要与 start 方法区分开

(发令枪响)调用 start 方法 , 才是真正在系统里创建出线程 , 才真正开始执行任务

2.4 中断线程(线程执行结束)

线程的执行结束 , 其实就是让线程的入口方法执行完成 , 线程也就执行结束了

普通线程的入口方法就是 run 方法
主线程的入口方法就是 main 方法

那么我们想要中断线程 , 其实就只需要把线程的入口方法执行结束即可
我们有两种方法来中断线程

2.4.1 手动创建的标志位

我们可以自己设置个标志位来区分线程是否要结束

public class Demo11 {// 用一个布尔变量表示线程是否要结束// 注意使用成员变量private static boolean flag = false;public static void main(String[] args) {Thread t = new Thread(() -> {while(!flag) {System.out.println("线程运行中...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("新线程执行结束...");});t.start();try {Thread.sleep(5000);//休息5s} catch (InterruptedException e) {e.printStackTrace();}System.out.println("这里可以控制新线程退出");flag = true;}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T23oPRJO-1678059222242)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220816151113932.png#id=QAeX9&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AxkFtmoh-1678059222242)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220816152245167.png#id=Levjt&originHeight=757&originWidth=1815&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

这种方案是可行的 , 其实 Thread 自己就提供了这种方法 , 内置了标志位 , 就不需要我们自己创建了

2.4.2 Thread自带的标志位

获取到当前线程的实例:Thread.currentThread() 
// currentThread是静态方法,'.'的方式就可以调用他的方法获取内置的标志位的值:Thread.currentThread().isInterrupted()
// 为true表示要被结束修改标志位(控制进程退出):t.interrupt();
public class Demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while(!Thread.currentThread().isInterrupted()) {System.out.println("线程运行中~");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();Thread.sleep(5000);System.out.println("控制新线程退出");t.interrupt();}
}

我们运行之后发现 , 5s之后程序报错了 , 报错之后还在继续运行
异常是出现了 , 但是线程好像还在继续进行
image.png
这里我们就需要理解 interrupt 方法的行为 :

  1. 如果 t 线程没有处在阻塞状态(处在运行状态) , 此时 interrupt 就会修改内置的标志位
    image.png
  2. 如果 t 线程处在阻塞状态 , 此时 interrupt 就会让线程内部产生异常的方法 , 例如 : interrupt 让线程里的 sleep 方法 , 抛出一个 InterruptedException 的异常.

异常被 catch 捕获了 , 但是捕获之后 , 啥也没干 , 只是打印了个调用栈
image.png
如果我们把打印栈注释掉 , 那么这次什么都不会打印
这就相当于把异常信息捕获之后 , 啥也没干 , 略过去了
image.png
正是因为这样的捕获行为 , 程序员就需要自己控制线程的退出行为了

  1. 可以立即退出
public class Demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while(!Thread.currentThread().isInterrupted()) {System.out.println("线程运行中~");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();// 1. 立即退出break;}}});t.start();Thread.sleep(5000);System.out.println("控制新线程退出");t.interrupt();}
}

image.png

  2. 也可以稍后退出
public class Demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("线程运行中~");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();// 2. 稍后退出System.out.println("新线程即将退出");try {Thread.sleep(5000);} catch (InterruptedException ex) {ex.printStackTrace();}break;}}System.out.println("新线程已经退出");});t.start();Thread.sleep(5000);System.out.println("控制新线程退出");t.interrupt();}
}

image.png

  3. 就不退出 : 啥都不干 , 就是忽略了异常

image.png
主线程发出 “退出” 命令的时候 , 新线程自己决定如何处理退出的行为
比如 : 室友叫你去食堂

  1. 立即跟室友去食堂 [立即退出]
  2. 打完这局再去 [稍后退出]
  3. 装没听见 [不去退出]

另外我们在定义的部分是这样说的 :
如果 t 线程处在阻塞状态 , 此时 interrupt 就会让线程内部产生阻塞的方法
他的意思就是 interrupt 不会自己抛出异常 , 他会在中间捣乱 , 让线程内部的 sleep 抛出异常
image.png


除了上述方法 , 判定标志位还有另一种方法 : Thread.interrupted()
这种方法的标志位会自动清除
比如 : 刚开始的时候 , 我们去控制他中断 , 这里的标志位先设为 true
等到读取的时候会读到这个 true , 但是读取完之后这个标志位就自动恢复成 false 了
就类似于开关 : 开关一按 , 自己就又弹起来了
而我们上面讲的 Thread.currentThread().isInterrupted()就是开关按下去 , 就不弹起来了

我再帮大家梳理一下思路
image.png
image.png

2.5 线程等待 join

2.5.1 join 是怎么回事?

我们知道 , 线程之间的执行顺序是完全随机的 , 是由系统决定的 .
但是我们对于这种随机性的东西很头疼 , 所以让顺序能够确定下来 , join 关键字就应运而生.
join 就是一种确定线程执行顺序的辅助手段
咱们虽然不能决定多个线程开始的顺序 , 但是这回有了 join , 我们就可以决定结束的顺序了.
还是看一下我们之前的代码

public class Demo8 {private static final long num = 20_0000_0000;public static void concurrency() {long begin_time = System.currentTimeMillis();Thread t1 = new Thread(() -> {long a = 0;for (long i = 0; i < num; i++) {a++;}});Thread t2 = new Thread(() -> {long b = 0;for (long i = 0; i < num; i++) {b++;}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}long end_time = System.currentTimeMillis();System.out.println("多线程消耗的时间:" + (end_time - begin_time) + "ms");}public static void main(String[] args) {concurrency();}
}

在这里面 , main t1 t2 三个线程是随机调度执行的
那么此处的需求 , 就是希望让 t1 和 t2 都执行完了之后 , main 再开始计时 , 来统计执行时间
也就是让 t1 和 t2 先执行完 , main 后执行完
虽然我们无法干预调度器的行为 , 但是我们可以控制 main 线程主动的去进行等待 , 所以我们在 main 线程中分别调用了 t1.join() [main 阻塞 , 等待 t1 执行完]、t2.join() [main 阻塞 , 等待 t2 执行完]
当 t1 t2 都执行完毕 , main 解除阻塞 , 然后程序继续向下执行 , 才能执行完

可以类比领导与职工 , 我们的 main 线程就是 领导 , 我们的 t1 线程和 t2 线程就是员工
早上上班 , 领导给两位职工派活
接下来 , 领导就一直摸鱼 , 等待 t1 和 t2 汇报成果
直到 t1 和 t2 完成工作 , main 线程拿过来验收 , 整个流程才算完事

谁先调用 join 谁后调用 join 都是无所谓的
image.png
我们想让谁阻塞 , 谁就调用 join 即可

比如我们想让 t1 以及 t2 阻塞
我们就在 main 线程中调用 t1.join() t2.join() 即可

但是 main 线程比较特殊 , 没有这样的写法 main.join(), 剩下其他自己创建的线程 , 都可以 join

另外 , join 自身也是有可能发生阻塞的 , 所以我们也要去处理一下异常

try {t1.join();t2.join();
} catch (InterruptedException e) {e.printStackTrace();
}

join 的行为 :

  1. 如果被等待的线程还没执行完 , 就阻塞等待
  2. 如果等待的线程已经执行完了 , 直接就返回

2.5.2 栗子1 : 让 main 等待 t2 , t2 去等待 t1

让 main 调用 t2.join() , 让 t2 调用 t1.join()

public class Demo13 {//先创建两个线程,但是不指向任何元素,方便后面访问private static Thread t1 = null;private static Thread t2 = null;public static void main(String[] args) throws InterruptedException {System.out.println("main开始");t1 = new Thread(() -> {System.out.println("t1开始");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1结束");});t1.start();t2 = new Thread(() -> {System.out.println("t2开始");try {t1.join();//让t2等待t1执行完再去执行} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2结束");});t2.start();t2.join();//main等待t2System.out.println("main结束");}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QZA2gNlt-1678059222246)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220816194716531.png#id=uCcEB&originHeight=1030&originWidth=1920&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

2.5.3 栗子2 : 控制 main 先运行 t1 , t1 执行完再运行 t2

实际上就是谁后调用 join , 谁就后结束
所以先调用 t1.join(), 后调用 t2.join() 就代表让 t1 先执行完 , t2 后执行完 , 就达到我们想要的结果了

public class Demo10 {public static void main(String[] args) throws InterruptedException {System.out.println("main 开始");Thread t1 = new Thread(() -> {System.out.println("t1 开始");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t1 结束");});t1.start();t1.join();Thread t2 = new Thread(() -> {System.out.println("t2 开始");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("t2 结束");});t2.start();t2.join();System.out.println("main 结束");}
}

image.png

2.5.4 带参数版本的 join

函数作用
public void join()死等线程结束 (不见不散)
public void join(long mills)设定了最大等待时间
public void join(long mills,int nanos)传了更加精确的时间

未来实际开发程序的时候 , 像这种的等待操作 , 一般不会采用死等的方式 , 这种方式有风险 ! 万一代码出了 bug 没控制好 , 死等就容易让服务器卡死 , 无法继续工作 .
其中 , public void join(long mills,int nanos)这个方法 , 第一个参数传的是毫秒 , 第二个参数传的是纳秒 , 比如第一个参数是 100 , 第二个参数是 1000 , 相当于等待了 100.001 ms

2.6 获取到线程引用 : Thread.currentThread()

为了对线程进行操作(线程等待、线程中断、获取各种线程的属性…) , 就需要获取到线程引用
这个方法我们刚才已经用到了

Thread.currentThread();

在线程操作之前 , 我们需要先获取到线程的引用 , 才能对线程进行具体操作

如果是继承 Thread , 然后重写 run 方法的话 , 可以直接在 run 中使用 this 即可获取到线程的实例
但是如果是 Runnable 或者 lambda , this 的方式就不可以了 , 这两种情况下 , this 就不是指向 Thread 实例

所以更通用的办法就是使用 Thread.currentThread(), 这是一个静态方法 , 哪个线程调用这个方法 , 得到的结果就是哪个线程的实例 , 以后获取到线程实例 , 我们就无脑使用这个方法即可

2.7 线程的休眠 : sleep

sleep 能让线程休眠一会
我们先讲解一下 相关的原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eO9Wnmyp-1678059222247)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220818193708646.png#id=zUjOC&originHeight=850&originWidth=1819&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

那么关于 PCB , 由于我们之前只介绍了进程 , 但是现在新学了线程 , PCB 这个概念还是需要重新提一下的

一组进程包含多组线程 , 每个线程都有自己的 PCB , 那么每一组进程其实就是对应一组 PCB 了
但是同一个进程里面的若干 PCB 还是有关联的

  1. PCB里面有一个 “线程组ID” , 是一样的
  2. PCB 里面的内存指针和文件描述符表 , 都是一样的

三 . 线程的状态

我们之前介绍过就绪状态与阻塞状态 , 在 Java / JVM 中 , 对于线程的状态 , 做了一个更明确的区分

NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了

2023128141314.bmp

3.1 通过代码来看线程的状态

public class Demo13 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {System.out.println("Hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}});// NEW 状态:// 在 start 之前获取// 获取到的是线程还未创建的状态System.out.println(t.getState());t.start();// RUNNABLE 状态// 线程就绪状态// 线程正在工作中System.out.println(t.getState());// TIMED_WAITING 状态// 线程调用了 sleep 方法Thread.sleep(500);System.out.println(t.getState());t.join();// 等待t线程结束// TERMINATED 状态// 在 join 之后获取// 获取到的是线程已经结束后的状态System.out.println(t.getState());}
}

image.png

3.2 线程状态和状态转移的意义

image.png
这个图看起来吓人 , 实际上我们可以简化成下面这样
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8IyvAVK7-1678059222248)(https://jialebihaitao.oss-cn-beijing.aliyuncs.com/image-20220818194836664.png#id=iNKMQ&originHeight=514&originWidth=1775&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]

我们再给大家简单介绍一个方法 : yield
yield 的作用是让调用者暂时放弃CPU , 回到阻塞队列里面去排队
我们可以粗暴的理解成 sleep(0)
用的不多 , 有印象即可
关于 yield , 举个栗子 :
我跟我的妈妈去超市 , 到结账的位置了 , 她突然想起来 “艾玛 , 忘打酱油了” , 让我继续排队 , 但是我都要交钱了她还没回来 , 这就很尴尬 . 那么 yield 就是我让后面结账的那个人先结账 , 我的妈妈要是还没回来 , 那就再让后面的人继续往前去结账 , 这就相当于我们一直没出就绪队列 . sleep(0)的意思就是我觉得不好意思了 , 刚退出队伍 , 我妈妈就赶回来了 , 我又赶紧回去队伍 , 这就是出了阻塞队列然后又立马回来了

yield 就是短暂的放弃 CPU , 排到就绪队列中的后面位置 (还是在就绪队列中 , 没进阻塞队列)
slepp(0) 就是马上进入阻塞队列 , 又马上回就绪队列 , 效果和 sleep 类似


http://www.ppmy.cn/news/30434.html

相关文章

Qt音视频开发20-vlc内核动态保存录像文件(不需要重新编译源码)

一、前言 在vlc默认提供的保存文件方式中&#xff0c;通过打开的时候传入指定的参数来保存文件&#xff0c;直到关闭播放生成文件&#xff0c;这种方式简单暴力&#xff0c;但是不适用大部分的场景&#xff0c;大部分时候需要的是提供开始录制和停止录制的功能&#xff0c;也就…

JavaScript RegExp 正则对象

文章目录JavaScript RegExp 正则对象RegExp 对象修饰符test()exec()方括号元字符量词RegExp 对象方法支持正则表达式的 String 对象的方法JavaScript RegExp 正则对象 RegExp&#xff1a;是正则表达式&#xff08;regular expression&#xff09;的简写。 RegExp 对象 正则表…

【精品】SpringBoot中基于拦截器实现登录验证功能

拦截器简介 拦截器是属于springmvc体系的&#xff0c;只能拦截controller的请求。拦截器&#xff08;Interceptor&#xff09;是一种动态拦截方法调用的机制&#xff0c;在SpringMVC中动态拦截控制器方法的执行。 Interceptor 作用 日志记录&#xff1a;记录请求信息的日志&…

浏览器主页被hao123劫持的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。喜欢通过博客创作的方式对所学的知识进行总结与归纳,不仅形成深入且独到的理…

基于jdk8的HashMap源码解析

hashMap常见面试题总览 为什么重写Equals还要重写HashCode方法&#xff1f;HashMap如何避免内存泄漏问题&#xff1f;HashMap1.7底层是如何实现的&#xff1f;HashMapKey为null存放在什么位置&#xff1f;HashMap如何解决Hash冲突问题&#xff1f;HashMap底层采用单链表还是双…

Java知识复习(十三)数据库和SQL

1、主键和外键 主键也叫主码。主键用于唯一标识一个元组&#xff0c;不能有重复&#xff0c;不允许为空。一个表只能有一个主键。外键也叫外码。外键用来和其他表建立联系用&#xff0c;外键是另一表的主键&#xff0c;外键是可以有重复的&#xff0c;可以是空值。一个表可以有…

Arduino添加ESP32开发板

【2023年3月4日】 最近要在新电脑上安装Arduino&#xff0c;需要进行一些配置&#xff0c;正好记录一下&#xff01; Arduino2.0.1 下的开发板添加操作。 ESP32开发板GitHub链接&#xff1a; GitHub - espressif/arduino-esp32: Arduino core for the ESP32Arduino core for…

Linux 进程:exec函数簇

目录&#xff08;1&#xff09;execl&#xff08;2&#xff09;execlp&#xff08;3&#xff09;execle&#xff08;4&#xff09;execv&#xff08;5&#xff09;execvp&#xff08;6&#xff09;execve在进程控制中提到&#xff0c;子进程的最大价值在于程序替换&#xff0c;…