当前位置: 首页 > news >正文

JAVA SE 多线程(上)

文章目录

    • 📕1. Thread类及常见方法
        • ✏️1.1 创建线程
        • ✏️1.2 Thread 的常见构造方法
        • ✏️1.3 Thread 的几个常见属性
        • ✏️1.4 启动一个线程---start()
        • ✏️1.5 中断一个线程---interrupt()
        • ✏️1.6 等待一个线程---join()
        • ✏️1.7 获取当前线程引用
        • ✏️1.8 休眠当前线程
    • 📕2. 线程的状态
    • 📕3. 线程安全
        • ✏️3.1 线程不安全案例
        • ✏️3.2 线程不安全的原因
    • 📕4. synchronized 关键字
        • ✏️4.1 synchronized的特性
        • ✏️4.2 synchronized 使用示例
    • 📕5. volatile关键字
    • 📕6. wait与notify
        • ✏️6.1 wait()方法
        • ✏️6.2 notify()方法
        • ✏️6.3 notifyAll()方法
    • 📕7. 单例模式
    • 📕8. 阻塞队列
        • ✏️8.1 生产者消费者模型
        • ✏️8.2 标准库中的阻塞队列
        • ✏️8.3 阻塞队列实现
    • 📕9. 定时器
    • 📕10.线程池
        • ✏️10.1 ExecutorService 和 Executors
        • ✏️10.2 ThreadPoolExecutor
        • ✏️10.3 线程池的工作流程

🌰首先,我们设想以下的一个场景:当一家公司去银行办理业务,既要进行财务转账,⼜要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五⼀起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理同一家公司的业务。

此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。

为什么要有线程呢?

  1. 首先, “并发编程” 成为 “刚需” ,
    单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源 . 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要用到并发编程.

  2. 其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量
    创建线程比创建进程更快.
    销毁线程比销毁进程更快.
    调度线程比调度进程更快

  3. 最后, 线程虽然比进程轻量, 但是人们还不满足 , 于是又有了 “线程池”(ThreadPool) 和 “协程”
    (Coroutine)

进程和线程的区别?

  1. 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  2. 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  3. 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
  4. 一个进程挂了一般不会影响到其他进程. 但是一个线程挂了, 可能把同进程内的其他线程一起带走(整个进程崩溃).

在这里插入图片描述

Java 的线程 和 操作系统线程 的关系?

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户
使用 , Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

📕1. Thread类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread 对象与之关联。

用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
在这里插入图片描述

✏️1.1 创建线程
  1. 继承Thread类
//继承Thread来创建一个线程类
class MyThread extends Thread{@Override//重新run方法,run方法中是该线程具体要做的任务public void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}
}
public class Test {public static void main(String[] args) {//实例化线程类对象MyThread t = new MyThread();//通过start()方法启动线程t.start();}
}
  1. 实现 Runnable 接口
class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}
}
public class Test {public static void main(String[] args){//创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数Thread t = new Thread(new MyRunnable());t.start();}
}
  1. 匿名内部类创建 Thread 子类对象
public class Test {public static void main(String[] args) {//匿名内部类创建Thread的子类Thread t = new Thread(){@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}};t.start();}
}
  1. 匿名内部类创建 Runnable 子类对象
public class Test {public static void main(String[] args) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(i);}}});t.start();}
}
  1. lambda 表达式创建 Runnable 子类对象
public class Test {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("这是一个用lambda表达式创建的线程");});t.start();}
}

强烈推荐!!!

✏️1.2 Thread 的常见构造方法
方法说明
Thread()创建线程对象
Thread(String name)创建线程对象并命名
Thread(Runnable target , String name)使用Runnable对象创建线程对象并命名
Thread(Runnable target)使用Runnable对象创建线程对象
✏️1.3 Thread 的几个常见属性

在这里插入图片描述

• ID 是线程的唯一标识,不同线程不会重复
• 名称是什么无所谓,不影响运行,是为了方便调试
• 状态表示线程当前所处的一个情况
• 优先级高的线程理论上来说更容易被调度到
• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
• 是否存活,即简单的理解,为 run 方法是否运行结束了

✏️1.4 启动一个线程—start()

我们现在已经知道如何通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

• 覆写 run 方法是提供给线程要做的事情的指令清单
• 线程对象可以认为是把 李四、王五叫过来了
• 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

调用 start 方法, 才真的在操作系统的底层创建出一个线程.

在这里插入图片描述

✏️1.5 中断一个线程—interrupt()
  1. 通过一个变量进行标记
public class Test {public static boolean flag = true;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(){@Overridepublic void run() {while(flag){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();System.out.println("hello main");Thread.sleep(3000);flag = false;System.out.println("让线程中断");}
}
  1. 调用 interrupt() 方法
public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{// 由于这个 currentThread 方法, 是在后续 t start 之后, 才执行的.// 并且是在 t 线程中执行的. 返回的结果就是指向 t 线程对象的引用了.while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});t.start();Thread.sleep(2000);//调用这个方法,就是把标志位由false改为truet.interrupt();}
}
//使用interrupt()方法的时候
//1. t线程没有进行sleep()阻塞时,t的isInterrupted()方法返回true,通过循环条件结束循环
//2. t线程进行sleep()阻塞时,t的isInterrupted()方法还是返回true,但是sleep()方法如果被提前唤醒,抛出InterruptedException异常,同时会把isInterrupted()方法设为false,此时就要手动决定是否要结束线程了
✏️1.6 等待一个线程—join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下⼀步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
在这里插入图片描述

public class Test {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for (int i = 0; i < 4; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();System.out.println("main线程开始了");t.join();System.out.println("main线程等t线程结束了");}
}
✏️1.7 获取当前线程引用

在这里插入图片描述

✏️1.8 休眠当前线程
方法解释
public static native void sleep(long millis) throws InterruptedException;休眠当前线程 , 以毫米为单位

📕2. 线程的状态

线程的状态是一个枚举类型:

• NEW: 安排了工作, 还未开始行动
• RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作
• BLOCKED: 由于加锁产生的阻塞
• WAITING: 无超时时间的阻塞
• TIMED_WAITING:有超时时间的阻塞
• TERMINATED: 工作完成了
在这里插入图片描述

📕3. 线程安全

✏️3.1 线程不安全案例

请大家观察下述代码:

public class Test {private static int count;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50_000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50_000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}

大家认为count最终的值会是100_000吗? 不是的,count最终的值是一个小于100_000的随机数.那为什么呢?

线程不安全的概念?

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

✏️3.2 线程不安全的原因
  1. 线程调度是随机的

  2. 修改共享数据
    即多个线程同时修改一个数据

  3. 原子性

什么是原子性?

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的⼈。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令

上述代码中的count++对应着3条指令:

  1. load : 从内存把数据读到 CPU
  2. add : 进行数据更新
  3. save : 把数据写回到 CPU

上述三条指令在多线程中就是有问题的指令.如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
在这里插入图片描述
将3种指令执行顺序枚举出我们发现:只有第一种和第二种是正确的

  1. 内存可见性

这里主要个大家介绍一下JMM模型,关于可见性内容请大家查阅目录找volatile关键字

Java 内存模型 (JMM—Java Memory Model):
Java虚拟机规范中定义了Java内存模型 , 目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
在这里插入图片描述
• 线程之间的共享变量存在 主内存 (Main Memory)
• 每一个线程都有自己的 “工作内存” (Working Memory)
• 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
• 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

因为每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 “副本”. 这就导致了此时修改线程1 的工作内存中的值, 线程2 的工作内存不⼀定会及时变化.

初始情况下, 两个线程的工作内存内容一致.
在这里插入图片描述
一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不⼀定能及时同步
在这里插入图片描述
此时就引入了三个问题:

1.为什么要整这么多内存呢?

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.
在这里插入图片描述
CPU的寄存器和缓存统称为工作内存,越往上,速度越快,空间越小,成本越高

2.为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍, 上万倍).

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了.效率就大大提高了.

3.那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??

答案就是⼀个字: 贵 , 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度⼜远远快于硬盘.
对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.
在这里插入图片描述

  1. 指令重排序

一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题的,可以少跑⼀次前台。这种叫做指令重排序.

关于指令重排序引发的线程不安全问题请查询目录到单例模式!!!

📕4. synchronized 关键字

✏️4.1 synchronized的特性
  1. 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.
在这里插入图片描述
synchronized用的锁是存在Java对象头里的。
在这里插入图片描述
可以粗略理解成, 每个对象在内存中存储的时候, 都存有⼀块内存表示当前的 “锁定” 状态(类似于厕所的 “有人/无人”).

如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.

如果当前是 “有人” 状态, 那么其他⼈无法使用, 只能排队

什么是阻塞等待呢?

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程,再来获取到这个锁.

假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.

  1. 可重入

synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题;

什么是自己把自己锁死?

//第一次加锁,命名为锁1
synchronized (locker){
//第二次尝试加锁,命名为锁2,但是此时加锁要等到锁1释放锁synchronized (locker){count++;}
}
//锁1释放锁的条件锁2中的代码要执行完,这就是自己把自己锁死了//理解一下这个场景,车钥匙在家里,家门钥匙在车里

但Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

🌰举个例子:加入我追x姑娘,此时x姑娘处于未加锁状态 , 我可以表白成功 , 其他人也可以表白成功 . 但是如果我表白成功了, 意味着x姑娘就处于加锁状态了 , 其他人在想表白是不可能成功的 , 但是我无论想在表白多少次 , x姑娘都会同意

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息:

• 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.

• 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

✏️4.2 synchronized 使用示例
  1. 修饰代码块
public class SynchronizedDemo {private Object locker = new Object();public void method() {synchronized (locker) {}}
}
  1. 直接修饰普通方法
public class SynchronizedDemo {public synchronized void methond() {}
}
  1. 修饰静态方法
public class SynchronizedDemo {public synchronized static void method() {}
}

📕5. volatile关键字

  1. 内存可见性
import java.util.Scanner;class Counter {public int flag = 0;
}
public class Test {public static void main(String[] args) throws InterruptedException {Counter count = new Counter();Thread t1 = new Thread(()->{while (count.flag == 0){System.out.println("it is t1 main thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});Scanner scanner = new Scanner(System.in);Thread t2 = new Thread(()->{System.out.println("please input a number");count.flag = scanner.nextInt();});t1.start();t2.start();t1.join();t2.join();}
}

在这个代码中:

• 创建两个线程 t1 和 t2

• t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.

• t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.

• 预期当用户输入非 0 的值的时候, t1 线程结束

结果发现输入任意一个数字后线程t1并没有停止(这就是一个bug)

这是因编译器自身优化导致的bug,当编译器发现我们频繁load的flag是一个值得时候,就会把flag方法工作内存上,就不再上主内存load了,但是我们突然修改flag的值,主内存修改了,但是t1线程的工作内存并没有修改

在这里插入图片描述
代码在写入 volatile 修饰的变量的时候:

• 改变线程工作内存中volatile变量副本的值
• 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

• 从主内存中读取volatile变量的最新值到线程的工作内存中
• 从工作内存中读取volatile变量的副本

前⾯我们讨论JMM模型时说了, 线程直接访问工作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况. 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

  1. volatile不能保证原子性

虽然volatile解决了内存可见性,但是volatile不是原子的,我们想解决原子性问题还要synchronized锁,volatile和synchronized是两个不同维度的问题

📕6. wait与notify

因为线程都是抢占式进行的,并没有固定的顺序,是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
例如一场篮球赛 : 我们要让A球员传球 , B球员拿到球后进行投篮

完成这个协调工作, 主要涉及到以下的方法:

• wait() / wait(long timeout): 让当前线程进入等待状态.

• notify() / notifyAll(): 唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法

✏️6.1 wait()方法

wait 做的事情:

• 使当前执行代码的线程进行等待. (把线程放到等待队列中)

• 释放当前的锁

• 满足一定条件时被唤醒, 重新尝试获取这个锁.

注意 : wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

• 其他线程调用该对象的 notify 方法.

• wait 等待时间超时 (wait 方法提供⼀个带有 timeout 参数的版本, 来指定等待时间).

• 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

✏️6.2 notify()方法

notify 方法是唤醒等待的线程.

• 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

• 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)

• 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

✏️6.3 notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程

notifyAll 一下全都唤醒, 需要这些线程重新竞争锁
在这里插入图片描述

📕7. 单例模式

首先我们要知道 , 什么是设计模式?

设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.

软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.

单例模式具体的实现方式有很多. 最常见的是 “饿汉” 和 “懒汉” 两种.

  1. 饿汉模式
//类加载的同时创建实例
class Singleton{private Singleton instance = new Singleton();private Singleton(){};public Singleton getInstance(){return instance;}
}
  1. 懒汉模式—单线程版
//类加载的时候不创建实例. 第一次使⽤的时候才创建实例
class Singleton{private static Singleton instence = null;private Singleton(){};public static Singleton getInstance(){if (instence == null){return new Singleton();}return instence;}
}
  1. 懒汉模式—多线程版❗❗❗
//使⽤双重 if 判定, 降低锁竞争的频率.
//给 instance 加上了 volatile.
class Singleton{private static Object locker = new Object();private static volatile Singleton instence = null;private Singleton(){};public static Singleton getInstance(){if (instence==null){synchronized (locker){if (instence == null){return new Singleton();}}}return instence;}
}

理解双重 if 判定:

加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.

当多线程首次调⽤ getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作.当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.

📕8. 阻塞队列

什么是阻塞队列?

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.

当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的⼀个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

✏️8.1 生产者消费者模型

生产者消费者模式就是通一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力. (削峰填谷)
  2. 阻塞队列也能使生产者和消费者之间 解耦.
✏️8.2 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可

  1. BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.

  2. put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.

  3. BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

生产者消费者模型:

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;public class Test {public static void main(String[] args) {BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();Thread producer = new Thread(()->{Random random = new Random();while (true){try {int value = random.nextInt(1000);blockingQueue.put(value);System.out.println("生产了:"+value);Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread consumer = new Thread(()->{while (true){try {int value = blockingQueue.take();System.out.println("消费了:"+value);Thread.sleep(6000);} catch (InterruptedException e) {e.printStackTrace();}}});producer.start();consumer.start();}
}
✏️8.3 阻塞队列实现

• 通过 “循环队列” 的方式来实现.

• 使用 synchronized 进行加锁控制.

• put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).

• take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)

public class BlockingQueue {private int[] items = new int[1000];private volatile int size = 0;private volatile int head = 0;private volatile int tail = 0;public void put(int value) throws InterruptedException {synchronized (this) {// 此处最好使⽤ while.// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能⼜已经队列满了就只能继续等待while (size == items.length) {wait();}items[tail] = value;tail = (tail + 1) % items.length;size++;notifyAll();}}public int take() throws InterruptedException {int ret = 0;synchronized (this) {while (size == 0) {wait();}ret = items[head];head = (head + 1) % items.length;size--;notifyAll();}return ret;}public synchronized int size() {return size;}
}

📕9. 定时器

定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.

比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.

比如⼀个 Map, 希望⾥⾯的某个 key 在 3s 之后过期(自动删除).

类似于这样的场景就需要用到定时器.

标准库中的定时器:

标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule , schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).

Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("hello");}
}, 3000);

📕10.线程池

虽然创建销毁线程比创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会比较低效.线程池就是为了解决这个问题. 如果某个线程不再使用了, 并不是真正把线程释放, 而是放到⼀个 “池子” 中, 下次如果需要用到线程就直接从池子中取, 不必通过系统来创建了.

✏️10.1 ExecutorService 和 Executors

ExecutorService 表示一个线程池实例.

Executors 是一个工厂类, 能够创建出几种不同风格的线程池.

ExecutorService 的 submit 方法能够向线程池中提交若干个任务.

 ExecutorService service = Executors.newFixedThreadPool(1);service.submit(()->{System.out.println("this is a service");});

Executors 创建线程池的几种方式:

newFixedThreadPool: 创建固定线程数的线程池

newCachedThreadPool: 创建线程数目动态增长的线程池.

newSingleThreadExecutor: 创建只包含单个线程的线程池

newScheduledThreadPool: 设定延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装

✏️10.2 ThreadPoolExecutor

ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.
在这里插入图片描述
把创建一个线程池想象成开个公司. 每个员工相当于一个线程.

corePoolSize: 正式员工的数量. (正式员工, 一旦录用, 永不辞退)

maximumPoolSize: 正式员工 + 临时工的数目. (临时工: 一段时间不干活, 就被辞退).

keepAliveTime: 临时工允许的空闲时间.

unit: keepaliveTime 的时间单位, 是秒, 分钟, 还是其他值.

workQueue: 传递任务的阻塞队列

threadFactory: 创建线程的工厂, 参与具体的创建线程⼯作

RejectedExecutionHandler: 拒绝策略, 如果任务量超出公司的负荷了接下来怎么处理.

◦ AbortPolicy(): 超过负荷, 直接抛出异常.
◦ CallerRunsPolicy(): 调用者负责处理
◦ DiscardOldestPolicy(): 丢弃队列中最老的任务.
◦ DiscardPolicy(): 丢弃新来的任务.

✏️10.3 线程池的工作流程

在这里插入图片描述

相关文章:

JAVA SE 多线程(上)

文章目录 &#x1f4d5;1. Thread类及常见方法✏️1.1 创建线程✏️1.2 Thread 的常见构造方法✏️1.3 Thread 的几个常见属性✏️1.4 启动一个线程---start()✏️1.5 中断一个线程---interrupt()✏️1.6 等待一个线程---join()✏️1.7 获取当前线程引用✏️1.8 休眠当前线程 &…...

基于Bootstrap 的网页html css 登录页制作成品

目录 前言 一、网页制作概述 二、登录页面 2.1 HTML内容 2.2 CSS样式 三、技术说明书 四、页面效果图 前言 ‌Bootstrap‌是一个用于快速开发Web应用程序和网站的前端框架&#xff0c;由Twitter的设计师Mark Otto和Jacob Thornton合作开发。 它基于HTML、CSS和JavaScri…...

AUTOSAR图解==>AUTOSAR_SRS_Transformer

AUTOSAR Transformer 详解 基于AUTOSAR标准的Transformer组件技术解析 目录 1. AUTOSAR Transformer 概述 1.1 Transformer的作用1.2 Transformer在AUTOSAR中的位置2. Transformer架构设计 2.1 整体架构2.2 类结构设计2.3 交互流程3. Transformer类型与实现 3.1 SOME/IP Transf…...

iOS APP启动页及广告页的实现

iOS APP启动页及广告页的实现涉及UI布局、数据加载、倒计时控制、广告跳转等多个关键环节。以下是我的一些使用心得&#xff1a; 1. UI实现方案 双Window方案 原理&#xff1a;同时创建两个Window&#xff0c;主Window位于底层&#xff0c;广告Window覆盖在其上。通过切换mak…...

图绘Linux:基础指令脉络阁

目录 Linux命令行介绍 目录操作 ls 目录所含文件信息 ls 常用选项 pwd 在那个目录下 cd 进入目录 mkdir 创建目录 文件操作 touch 创建普通文件 echo向文件写入 cat 输出文件内容 cp 拷贝文件/目录 mv剪切重命名 rm 删除文件/目录 查找 * 匹配符 man 查找指令 …...

数字格式化库 accounting.js的使用说明

accounting.js 是一个用于格式化数字、货币和金额的轻量级库&#xff0c;特别适合财务和会计应用。以下是其详细使用说明&#xff1a; 安装与引入 通过 npm 安装&#xff1a; bash 复制 下载 npm install accounting 引入&#xff1a; javascript 复制 下载 const accounting …...

ngx_http_proxy_protocol_vendor_module 模块

一、前置要求 启用 PROXY 协议 在 listen 指令中添加 proxy_protocol 参数&#xff0c;例如&#xff1a; server {listen 80 proxy_protocol;listen 443 ssl proxy_protocol;… }商业订阅 本模块仅在 Nginx 商业版中提供。 二、示例配置 http {# 将 GCP 的 PSC 连接 ID 添…...

C++11-(2)

文章目录 &#xff08;一&#xff09;C11新增功能1.1 引用折叠1.1.1 在模板中使用引用折叠的场景1.1.2 引用折叠是如何实现的 1.2 完美转发1.3 lambda表达式语法1.3.1 定义1.3.2 lambda的使用场景1.3.3 捕捉列表1.3.4 mutable语法1.3.5 lambda的原理 &#xff08;一&#xff09…...

LeetCode算 法 实 战 - - - 双 指 针 与 移 除 元 素、快 慢 指 针 与 删 除 有 序 数 组 中 的 重 复 项

LeetCode算 法 实 战 - - - 双 指 针 与 移 除 元 素、快 慢 指 针 与 删 除 有 序 数 组 中 的 重 复 项 第 一 题 - - - 移 除 元 素方 法 一 - - - 双 重 循 环方 法 二 - - - 双 指 针方 法 三 - - - 相 向 双 指 针&#xff08;面 对 面 移 动&#xff09; 第 二 题 - - -…...

QT6 源(106):阅读与注释重要的基类控件 QWidget,这是其精简版,完整注释版为篇 37

&#xff08;1&#xff09;原篇幅 37 为最开始整理&#xff0c;整理的不是太完善。重点不突出。故重新整理&#xff0c;但删除了大量的注释&#xff0c;重在突出本 QWidget类的内部逻辑&#xff0c;更易观察其包含了哪些内容。至于不理解的成员函数与属性&#xff0c;内容已不太…...

【Bluedroid】蓝牙HID DEVICE错误报告处理全流程源码解析

本文基于Android蓝牙协议栈代码&#xff0c;深入解析HID设备在接收非法指令&#xff08;如无效的SET_REPORT&#xff09;时的错误处理全流程&#xff0c;涵盖错误映射、协议封装、传输控制三大核心模块。重点剖析以下机制&#xff1a; HID协议规范错误码的动态转换策略 控制通…...

Day29 类的装饰器

类也有修饰器&#xff0c;他的逻辑类似&#xff1a;接收一个类&#xff0c;返回一个修改后的类。例如 添加新的方法或属性&#xff08;如示例中的 log 方法&#xff09;。修改原有方法&#xff08;如替换 init 方法&#xff0c;添加日志&#xff09;。甚至可以返回一个全新的类…...

学习黑客Active Directory 入门指南(二)

Active Directory 入门指南&#xff08;二&#xff09;&#xff1a;深入逻辑结构与物理组件 &#x1f333;&#x1f3e2; 大家好&#xff01;欢迎回到 “Active Directory 入门指南” 系列的第二篇。在上一篇中&#xff0c;我们初步认识了Active Directory&#xff0c;了解了其…...

为什么el-select组件在下拉选择后无法赋值

为什么el-select组件在下拉选择后无法赋值 https://blog.csdn.net/ZHENGCHUNJUN/article/details/127325558 这个链接解决了大模型无法解决的问题 大模型能够写基础且高级一些的代码&#xff0c;但是遇到再深入一些的问题&#xff0c;还是得问百度。对于我这种小白来说问题原因…...

FreeRTOS的学习记录(临界区保护,调度器挂起与恢复)

临界区 在 FreeRTOS 中&#xff0c;临界区&#xff08;Critical Section&#xff09; 是指程序中一段必须以原子方式执行的代码区域&#xff0c;在此区域内不允许发生任务切换或中断干扰&#xff0c;以保护共享资源或执行关键操作。FreeRTOS 提供了多种机制来实现临界区&#…...

给个人程序加上MCP翅膀

背景 最近MCP这个词真是到处都是&#xff0c;看起来特别高大上。我平时没事的时候也一直在关注这方面的技术&#xff0c;知道它是怎么一回事&#xff0c;也懂该怎么去实现。但可惜一直抽不出时间来自己动手搞一个MCP服务。网上关于MCP的教程一搜一大把&#xff0c;但基本上都是…...

2023年河南CCPC(ABCEFHK)

文章目录 2023河南CCPCA. 小水獭游河南(字符串)B. Art for Rest(数组切割)C. Toxel与随机数生成器(水)E. 矩阵游戏&#xff08;dp&#xff09;F. Art for Last(区间最小差分)H. Travel Begins(数学思维)K. 排列与质数&#xff08;规律&#xff09;总结 2023河南CCPC A. 小水獭…...

【 Redis | 实战篇 秒杀优化 】

目录 前言&#xff1a; 1.分布式锁 1.1.分布式锁的原理与方案 1.2.Redis的String结构实现分布式锁 1.3.锁误删问题 1.4.锁的原子性操作问题 1.5.Lua脚本解决原子性问题 1.6.基于String实现分布式锁存在的问题 1.7.Redisson分布式锁 2.秒杀优化 3.秒杀的异步优化 3.1…...

【Spring】核心机制:IOC与DI深度解析

目录 1.前言 2.正文 2.1三层架构 2.2Spring核心思想&#xff08;IOC与AOP&#xff09; 2.3两类注解&#xff1a;组件标识与配置 2.3.1五大类注解 2.3.1.1Controller 2.3.1.2Service 2.3.1.3Repository 2.3.1.4Configuration 2.3.1.5Component 2.3.2方法注解&#x…...

1-机器学习的基本概念

文章目录 一、机器学习的步骤Step1 - Function with unknownStep2 - Define Loss from Training DataStep3 - Optimization 二、机器学习的改进Q1 - 线性模型有一些缺点Q2 - 重新诠释机器学习的三步Q3 - 机器学习的扩展Q4 - 过拟合问题&#xff08;Overfitting&#xff09; 一、…...

ARM A64 STR指令

ARM A64 STR指令 1 STR (immediate)1.1 Post-index1.1.1 32-bit variant1.1.2 64-bit variant 1.2 Pre-index1.2.1 32-bit variant1.2.2 64-bit variant 1.3 Unsigned offset1.3.1 32-bit variant1.3.2 64-bit variant 1.4 Assembler symbols 2 STR (register)2.1 32-bit varia…...

虚幻引擎5-Unreal Engine笔记之`GameMode`、`关卡(Level)` 和 `关卡蓝图(Level Blueprint)`的关系

虚幻引擎5-Unreal Engine笔记之GameMode、关卡&#xff08;Level&#xff09; 和 关卡蓝图&#xff08;Level Blueprint&#xff09;的关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之GameMode、关卡&#xff08;Level&#xff09; 和 关卡蓝图&#xff08;Level B…...

软件工具:批量图片区域识别+重命名文件的方法,发票识别和区域选择方法参考,基于阿里云实现

基于阿里云的批量图片区域识别与重命名解决方案 图像识别重命名 应用场景 ​​企业档案管理​​&#xff1a;批量处理扫描的合同、文件等图片&#xff0c;根据合同编号、文件标题等关键信息重命名文件​​医疗影像处理​​&#xff1a;识别X光、CT等医学影像中的患者ID、检查日…...

.NET外挂系列:1. harmony 基本原理和骨架分析

一&#xff1a;背景 1. 讲故事 为什么要开这么一个系列&#xff0c;是因为他可以对 .NET SDK 中的方法进行外挂&#xff0c;这种技术对解决程序的一些疑难杂症特别有用&#xff0c;在.NET高级调试 领域下大显神威&#xff0c;在我的训练营里也是花了一些篇幅来说这个&#xf…...

深入理解位图(Bit - set):概念、实现与应用

目录 引言 一、位图概念 &#xff08;一&#xff09;基本原理 &#xff08;二&#xff09;适用场景 二、位图的实现&#xff08;C 代码示例&#xff09; 三、位图应用 1. 快速查找某个数据是否在一个集合中 2. 排序 去重 3. 求两个集合的交集、并集等 4. 操作系…...

React Flow 边事件处理实战:鼠标事件、键盘操作及连接规则设置(附完整代码)

本文为《React Agent&#xff1a;从零开始构建 AI 智能体》专栏系列文章。 专栏地址&#xff1a;https://blog.csdn.net/suiyingy/category_12933485.html。项目地址&#xff1a;https://gitee.com/fgai/react-agent&#xff08;含完整代码示​例与实战源&#xff09;。完整介绍…...

【计算机网络】第一章:计算机网络体系结构

本篇笔记课程来源&#xff1a;王道计算机考研 计算机网络 【计算机网络】第一章&#xff1a;计算机网络体系结构 一、计算机网络的概念1. 理论2. 计算机网络、互连网、互联网的区别 二、计算机网络的组成、功能1. 组成2. 功能 三、交换技术1. 电路交换2. 报文交换3. 分组交换4.…...

实战设计模式之状态模式

概述 作为一种行为设计模式&#xff0c;状态模式允许对象在其内部状态改变时&#xff0c;改变其行为。这种模式通过将状态逻辑从对象中分离出来&#xff0c;并封装到独立的状态类中来实现。每个状态类代表一种特定的状态&#xff0c;拥有自己的一套行为方法。当对象的状态发生变…...

[C++入门]类和对象中(2)日期计算器的实现

目录 一、运算符重载 1、格式 2、简单举例 2、前置&#xff0c;后置 3、日期生成器的实现 1、声明与定义 1、友元函数 2、print函数 3、运算符重载 4、GetMonthDay 5、&#xff0c;-&#xff0c;&#xff0c;-的实现 6、重载流操作符 2、实现 3、定义源码 一、运算…...

数据质量问题的形成与解决

在数字化时代&#xff0c;数据已成为企业和组织发展的核心资产&#xff0c;数据质量的高低直接影响着决策的准确性、业务的高效性以及系统的稳定性。然而&#xff0c;数据质量问题频发&#xff0c;严重阻碍了数据价值的充分发挥。 一、数据质量问题的成因分析 1.信息因素 元数…...

论文阅读(四):Agglomerative Transformer for Human-Object Interaction Detection

论文来源&#xff1a;ICCV&#xff08;2023&#xff09; 项目地址&#xff1a;https://github.com/six6607/AGER.git 1.研究背景 人机交互&#xff08;HOI&#xff09;检测需要同时定位人与物体对并识别其交互关系&#xff0c;核心挑战在于区分相似交互的细微视觉差异&#…...

【机器学习】工具入门:飞牛启动Dify Ollama Deepseek

很久没有更新文章了,最近正好需要研究一些机器学习的东西&#xff0c;打算研究一下 difyOllama 以下是基于FN 的dify本地化部署&#xff0c;当然这也可能是全网唯一的飞牛部署dify手册 部署 官方手册&#xff1a;https://docs.dify.ai/en/getting-started/install-self-hos…...

课外活动:再次理解页面实例化PO对象的魔法方法__getattr__

课外活动&#xff1a;再次理解页面实例化PO对象的魔法方法__getattr__ 一、动态属性访问机制解析 1.1 核心实现原理 class Page:def __getattr__(self, loc):"""魔法方法拦截未定义属性访问"""if loc not in self.locators.keys():raise Exce…...

面试题总结二

1.mybatis三个范式 第一范式&#xff1a;表中字段不能再分&#xff0c;每行数据都是唯一的第二范式&#xff1a;满足第一范式&#xff0c;非主键字段只依赖于主键第三范式&#xff1a;满足第二范式&#xff0c;非主键字段没有传递依赖 2.MySQL数据库引擎有哪些 InnoDB&#…...

代码随想录算法训练营第六十六天| 图论11—卡码网97. 小明逛公园,127. 骑士的攻击

继续补&#xff0c;又是两个新算法&#xff0c;继续进行勉强理解&#xff0c;也是训练营最后一天了&#xff0c;六十多天的刷题告一段落了&#xff01; 97. 小明逛公园 97. 小明逛公园 感觉还是有点难理解原理 Floyd 算法对边的权值正负没有要求&#xff0c;都可以处理。核心…...

编程技能:字符串函数07,strncat

专栏导航 本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏&#xff0c;故划分为两个专栏导航。读者可以自行选择前往哪个专栏。 &#xff08;一&#xff09;WIn32 专栏导航 上一篇&#xff1a;编程技能&#xff1a;字符串函数06&#xff0c;strcat 回到目录…...

[Java实战]Spring Boot整合RabbitMQ:实现异步通信与消息确认机制(二十七)

[Java实战]Spring Boot整合RabbitMQ&#xff1a;实现异步通信与消息确认机制&#xff08;二十七&#xff09; 摘要&#xff1a;本文通过完整案例演示Spring Boot与RabbitMQ的整合过程&#xff0c;深入讲解异步通信原理与消息可靠性保证机制。包含交换机类型选择、消息持久化配…...

数据库中关于查询选课问题的解法

前言 今天上午起来复习了老师上课讲的选课问题。我总结了三个解法以及一点注意事项。 选课问题介绍 简单来说就是查询某某同学没有选或者选了什么课。然后查询出该同学的姓名&#xff0c;学号&#xff0c;课程号&#xff0c;课程名之类的。 sql文件我上传了。大家可以尝试练…...

用 UniApp 开发 TilePuzzle:一个由 CodeBuddy 主动驱动的拼图小游戏

我正在参加CodeBuddy「首席试玩官」内容创作大赛&#xff0c;本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 起心动念&#xff1a;从一个小游戏想法开始 最近在使用 UniApp 做练手项目的时候&#xff0c;我萌生了一个小小…...

golang 安装gin包、创建路由基本总结

文章目录 一、安装gin包和热加载包二、路由简单场景总结 一、安装gin包和热加载包 首先终端新建一个main.go然后go mod init ‘项目名称’执行以下命令 安装gin包 go get -u github.com/gin-gonic/gin终端安装热加载包 go get github.com/pilu/fresh终端输入fresh 运行 &…...

组态王|组态王中如何添加西门子1200设备

哈喽,你好啊,我是雷工! 最近使用组态王采集设备数据,设备的控制器为西门子的1214CPU, 这里边实施边记录,以下为在组态王中添加西门子1200PLC的笔记。 1、新建 在组态王工程浏览器中选择【设备】→点击【新建】。 2、选择设备 和设备建立通讯要通过对应的设备驱动。 在…...

碎片笔记|PromptStealer复现要点(附Docker简单实用教程)

前言&#xff1a;本篇博客记录PromptStealer复现历程&#xff0c;主要分享环境配置过程中的一些经验。 论文信息&#xff1a;Prompt Stealing Attacks Against Text-to-Image Generation Models. USENIX, 2024. 开源代码&#xff1a;https://github.com/verazuo/prompt-stealin…...

Docker配置SRS服务器 ,ffmpeg使用rtmp协议推流+vlc拉流

目录 演示视频 前期配置 Docker配置 ffmpeg配置 vlc配置 下载并运行 SRS 服务 推拉流流程实现 演示视频 2025-05-18 21-48-01 前期配置 Docker配置 运行 SRS 建议使用 Docker 配置 Docker 请移步&#xff1a; 一篇就够&#xff01;Windows上Docker Desktop安装 汉化完整指…...

c++学习之--- list

目录 ​编辑 一、list的定义: 二、list的模拟实现&#xff1a; 1、list的基本框架&#xff1a; 2、list的普通迭代器&#xff1a; 设计思想&#xff1a; 迭代器的一个特殊需求&#xff08;c 对于重载->的一颗语法糖&#xff09;&#xff1a; 代码实现&#xff1a; 3、cons…...

【C++】set、map 容器的使用

文章目录 1. set 和 multiset 的使用1.1 set类的介绍1.2 set的构造和迭代器1.3 set 的增删查1.4 insert和迭代器调用示例1.5 find和erase使用示例1.6 multiset和set的差异 2. map 和 multimap 的使用2.1 map 类的介绍2.2 pair 类型介绍2.3 map 的构造和迭代器2.4 map 的增删查2…...

实习记录小程序|基于SSM+Vue的实习记录小程序设计与实现(源码+数据库+文档)

实习记录小程序 目录 基于SSM的习记录小程序设计与实现 一、前言 二、系统设计 三、系统功能设计 1、小程序端&#xff1a; 2、后台 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大厂码…...

Git从入门到精通

Git 是什么 Git 是一个分布式版本控制系统&#xff0c;主要用于跟踪和管理文件&#xff08;尤其是代码&#xff09;的变更。 Git的下载与安装 进入git官网下载界面,选择Windows系统。 点击选择Git for Windows/x64 Setup,进行安装。 注意: Git GUI 是Git提供的一个图形界面工…...

Binary Prediction with a Rainfall Dataset-(回归+特征工程+xgb)

Binary Prediction with a Rainfall Dataset 题意&#xff1a; 给你每天的天气信息&#xff0c;让你预测降雨量。 数据处理&#xff1a; 1.根据特征值构造天气降雨量的新特征值 2.根据时间构造月和季节特征 3.处理缺失值 建立模型&#xff1a; 1.建立lightgbm模型 2.建立…...

【C++】unordered_map与set的模拟实现

unordered系列map和set&#xff0c;与普通区别 用法几乎相同&#xff0c;键值唯一&#xff0c;区别unordered系列迭代器是单向的并且遍历出来不是有序的。unordered系列在数据规模大且无序的情况下性能更优 底层实现&#xff1a; map 和 set &#xff1a;基于平衡二叉树&…...

老旧设备升级利器:Modbus TCP转 Profinet让能效监控更智能

在工业自动化领域&#xff0c;ModbusTCP和Profinet是两种常见的通讯协议。Profinet是西门子公司推出的基于以太网的实时工业以太网标准&#xff0c;而Modbus则是由施耐德电气提出的全球首个真正开放的、应用于电子控制器上的现场总线协议。这两种协议各有各的优点&#xff0c;但…...