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

Java中的线程安全问题(如果想知道Java中有关线程安全问题的基本知识,那么只看这一篇就足够了!)

        前言:多线程编程已经广泛开始使用,其可以充分利用系统资源来提升效率,但是线程安全问题也随之出现,它直接影响了程序的正确性和稳定性,需要对其进行深入的理解与解决。


✨✨✨这里是秋刀鱼不做梦的BLOG

✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客

在正式开始讲解之前,先让我们看一下本文大致的讲解内容:

目录

1.线程不安全概念及其原因

2.原子性问题

3.可见性问题

4.指令重排序问题

5.线程不安全的解决方案

        (1)synchronized关键字

补充:synchronized关键字的可重入性:

        (2)volatile关键字


1.线程不安全概念及其原因

        在多线程编程中,线程安全是一个至关重要的概念,当多个线程同时访问和操作共享数据时,如果没有适当的同步机制,可能会导致程序出现意想不到的结果。

        下面通过一个简单的代码示例来观察线程不安全现象:

// 此处定义一个int类型的变量
private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {// 对count变量进行自增5w次for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {// 对count变量进行自增5w次for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();// 如果没有这俩join,肯定不行的。线程还没自增完,就开始打印了。很可能打印出来的count值小于预期t1.join();t2.join();// 预期结果应该是10wSystem.out.println("count: " + count);
}

        在上述代码中,我们创建了两个线程 t1 和 t2,它们都试图对共享变量 count 进行大量的自增操作,理论上,当两个线程都完成任务后,count 的值应该达到100000。然而,实际运行结果却常常小于这个预期值(读者可以复制代码在编译器中自行尝试一下),这便是典型的线程不安全现象。

        ——那么,为何会出现这种情况呢?原因有如下两个:

  1. 线程调度的随机性:线程调度是由操作系统掌控的,它会在多个线程之间随机地切换执行权。在上述代码场景中,t1t2 线程极有可能交替执行自增操作。例如,t1 线程读取了 count 的当前值(假设为0),但在执行自增操作(count++)之前,线程调度器暂停了 t1 线程,并切换到 t2 线程。此时,t2 线程同样读取到 count 的值为0,随后执行自增操作,将 count 的值更新为1。接着,t1 线程恢复执行,可它依然使用之前读取到的0进行自增操作,最终将 count 的值更新为1,而非预期的2。

  2. 多个线程修改同一变量:当多个线程同时对同一个共享变量进行写操作,且没有任何同步保障时,数据的不一致性便极易出现,在当前例子中,t1t2 都在对 count 变量进行修改,它们的操作相互干扰,最终致使结果出现偏差。

        至此,我们通过上述的讲解,我们就大致的了解了到底什么是多线程中的线程不安全以及产生线程不安全的原因了。

2.原子性问题

        在多线程中,除了上述我们讲解的当我们有多个线程同时对同一个数据进行操作从而引起的线程安全问题外,原子性问题也是可能引起线程不安全的原因,那么什么是原子性问题呢?

原子性的概念:

        原子性,从本质上讲,是指一个操作或者一组操作作为一个不可分割的整体,其执行过程要么全部成功完成,要么全部不执行,绝不存在被其他线程中断的中间状态。在多线程环境中,倘若一个操作不具备原子性,那么就极有可能出现部分执行的状况,进而导致数据错误。

        这里我们还是使用上述的两个线程各自增加count5w次的例子来进行讲解,这里再让我们看一下上述的代码:

// 定义一个共享的int类型变量count,并初始化为0
private static int count = 0;public static void main(String[] args) throws InterruptedException {// 创建第一个线程t1,其任务是对count进行50000次自增操作Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});// 创建第二个线程t2,同样对count进行50000次自增操作Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});// 启动线程t1t1.start();// 启动线程t2t2.start();// 调用t1线程的join方法,确保t1线程执行完毕t1.join();// 调用t2线程的join方法,确保t2线程执行完毕t2.join();// 预期count的值应该是100000,但实际结果往往并非如此System.out.println("count: " + count);
}

        在上述代码中的 count++ 操作,看似简单的自增指令,实际上并非原子操作,在Java语言中,count++ 大致可分解为以下三个步骤:

  • 首先,读取 count 的当前值。

  • 接着,将读取到的值加1。

  • 最后,将计算后的新值写回 count

        假设 t1 和 t2 线程同时执行 count++ 操作,就可能出现如下情形:t1 线程读取了 count 的初始值为0,然而在执行加1操作之前,线程调度器切换到了 t2 线程。t2 线程同样读取到 count 的值为0,随后进行加1操作并将结果1写回 count,此时,count 的值变为1,接着,t1 线程恢复执行,它依旧使用之前读取到的0进行加1操作,得到结果1,并将其写回 count

        如此一来,最终 count 的值仅增加了1,而非预期的2。这便是因为 count++ 操作不具备原子性,在执行过程中被其他线程中断,从而导致了错误的结果。

        ——这就是所谓的原子性问题。

3.可见性问题

        在了解完上述的两种造成多线程中的线程安全问题的原因之后,在让我们看一下另一种造成多线程线程安全的原因——内存可见性问题

        ——那么什么是内存可见性问题(可见性问题)呢?

可见性的概念:

        可见性,简单来说,是指一个线程对共享变量值的修改,能够及时且准确地被其他线程察觉到。在多线程编程的情境下,如果一个线程修改了共享变量的值,但是其他线程无法立即获取到这个修改后的最新值,那么就会产生可见性问题。

        当然,提到内存可见性问题就不得不提及Java内存模型,那么Java内存模型和内存可见性问题又有什么联系呢?

Java内存模型与可见性问题的关系:

        Java内存模型(JMM)明确规定了Java程序中变量的访问规则。每个线程都拥有自己独立的工作内存,当线程需要读取一个共享变量时,会首先将变量从主内存拷贝到自己的工作内存,然后再从工作内存中读取数据;而当线程要修改一个共享变量时,会先在工作内存中修改其副本,之后再将修改后的值同步回主内存。

        由于每个线程的工作内存相互独立,这就可能导致一种情况:一个线程修改了共享变量的值,但这个修改尚未及时同步到主内存,或者其他线程还未从主内存更新自己工作内存中的副本,从而致使其他线程无法看到该变量的最新值。

        这样我们就大致的了解了什么是Java内存模型,以及Java内存模型与可见性问题的关系了。

        我相信读者在看到这里的时候脑子里只用一个想法,我勒个去,上边这都是什么和什么啊?根本看不懂啊!没关系,接下来让我们使用一个例子来帮助你更好的理解上述内存可见性问题。

案例代码:

static class Counter {public int flag = 0;
}public static void main(String[] args) {Counter counter = new Counter();Thread t1 = new Thread(() -> {while (counter.flag == 0) {// 线程t1在此处循环等待,直到flag的值变为非0}System.out.println("循环结束!");});Thread t2 = new Thread(() -> {Scanner scanner = new Scanner(System.in);System.out.println("输入一个整数:");counter.flag = scanner.nextInt();});t1.start();t2.start();
}

        在这个例子中,t1 线程在一个循环里持续检查 counter.flag 的值是否为0,如果是,则持续循环等待;t2 线程等待用户输入一个整数,并将其赋值给 counter.flag。按照预期,当用户输入非0的值时,t1 线程应当结束循环并打印 "循环结束!"。

        然而,实际情况可能是,即便 t2 线程已经修改了 counter.flag 的值,t1 线程却并未立即察觉到这个变化,依旧在循环中持续等待。这是因为 t1 线程可能始终在使用自己工作内存中的 counter.flag 副本,而没有及时从主内存更新该副本,从而引发了可见性问题。

        至此,我相信读者通过上述的案例讲解之后,就对内存可见性问题有了进一步理解了!!!

4.指令重排序问题

        讲解完上述三种产生多线程问题的原因之后,还有没有其他的可能产生多线程线程安全的原因呢?还真有,其就是指令重排序问题。

指令重排序的概念:

        指令重排序是指编译器或处理器为了优化程序的性能,在不改变单线程程序语义的前提下,对指令的执行顺序进行重新排列。在单线程环境中,指令重排序通常不会引发问题,因为程序的执行结果是确定的。然而,在多线程环境下,指令重排序可能会改变代码的执行顺序,进而导致线程安全问题。

        这里我们也是使用一个案例来帮助读者来进一步理解指令重排序问题。

// 定义两个共享变量
private static boolean initialized = false;
private static int value;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {value = 42;initialized = true;});Thread t2 = new Thread(() -> {if (initialized) {System.out.println("value: " + value);}});t1.start();t2.start();t1.join();t2.join();
}

        在这个例子中,t1 线程首先对 value 赋值为42,随后将 initialized 设置为 truet2 线程则检查 initialized 的值,如果为 true,就打印 value 的值。由于指令重排序的存在,t1 线程中的指令可能会被重新排序。

        例如,initialized = true 可能会在 value = 42 之前执行。这样一来,当 t2 线程检查 initialized 的值为 true 时,value 的值可能还未被正确赋值,从而导致打印出错误的结果(可能是0,而不是42)。

        这样我们就了解了什么是指令重排序问题了。

5.线程不安全的解决方案

        学习完上述可能产生线程安全的原因之后,接下来就让我们学习一下如何去在多线程编程中防止程序发生线程安全问题。

        (1)synchronized关键字

在学习如何使用synchronized关键字之前,先让我们看一下synchronized关键字是什么:

synchronized 关键字具有强大的互斥特性。当一个线程进入一个对象的 synchronized 方法或代码块时,其他线程若试图进入同一个对象的 synchronized 方法或代码块,将会被阻塞等待,直到持有锁的线程释放锁为止。

这里我们使用一个例子来进行讲解:

public class Demo2 {public static int number = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread thread1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {synchronized (locker) {number++;}}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {synchronized (locker) {number++;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(number);}
}

代码解析:

  • 静态变量 number 用于存储共享计数。
  • 使用 Object locker 作为同步锁,确保对 number 的修改是线程安全的。
  • 创建两个线程,每个线程循环 1000 次,通过 synchronized (locker) 代码块安全地增加 number
  • 启动两个线程并等待它们结束,最后输出 number 的值。

        通过上述的案例,我相信读者就可以对synchronized关键字有一定的理解了!

        当然synchronized关键字还可以修饰方法,当修饰普通方法时,锁对象为当前对象(this);修饰静态方法时,锁对象为类对象(class),例如:

public class SynchronizedMethodDemo {private static int count = 0;// 修饰普通方法,锁对象为thispublic synchronized void increment() {count++;}// 修饰静态方法,锁对象为类对象public synchronized static void staticIncrement() {count++;}
}

        需要特别注意的是,使用 synchronized 关键字会带来一定的性能开销,因为获取和释放锁的过程需要消耗时间。因此,在实际应用中,应尽可能缩小同步代码块的范围,仅在必要之处进行同步操作,以此提高程序的性能。

补充:synchronized关键字的可重入性:

        这里我们先给出可重入性的简介:

可重入性是指当一个线程已经获得了某个对象的锁后,它可以再次获得这个锁,而不会被阻塞

例如,当一个线程调用一个 synchronized 方法时,若该方法内部又调用了另一个 synchronized 方法,此时该线程能够继续获取锁并执行内部的 synchronized 方法,而不会被自身阻塞。这是因为在可重入锁的内部机制中,包含了“线程持有者”和“计数器”两个重要信息,当某个线程加锁时,若发现锁已被自己占用,那么它仍然可以顺利获取锁,并使计数器自增。只有当计数器递减为0时,锁才会真正被释放,从而允许其他线程获取该锁。

        可重入性的特点:

  1. 锁的重复获取:同一个线程可以多次获取同一个锁,而不会导致死锁。例如,如果线程 A 已经获得了对象 O 的锁,那么它可以再次进入 O 的同步方法或同步块。

  2. 计数机制:Java 的 synchronized 内部使用了一个计数机制。当一个线程获得锁时,计数器加一;当线程释放锁时,计数器减一。当计数器为零时,锁被释放。 ​​​​​​​

        如果读者看了上述的文字解释之后还是不太理解,那么我们接下看使用一个例子来帮助你进一步理解synchronized的可重入性:

public class ReentrantExample {synchronized void methodA() {System.out.println("Method A is called");methodB(); // 可以在这里调用同一个对象的另一个同步方法}synchronized void methodB() {System.out.println("Method B is called");}public static void main(String[] args) {ReentrantExample example = new ReentrantExample();example.methodA(); // 调用 methodA}
}

        在上面的例子中,当 methodA 被调用时,线程获得了锁并执行 methodA,然后可以安全地调用 methodB,因为它已经持有了该对象的锁,这就是synchronized的可重入性。

        (2)volatile关键字

在了解完了synchronized关键字之后,让我们了解一下volatile关键字,首先先让我们了解一下什么是volatile关键字:

volatile 关键字的核心作用是保证内存可见性。它强制线程在读写共享变量时,必须直接从主内存读取或写入,而不能使用工作内存中的副本。当一个线程修改了 volatile 修饰的变量时,它会立即将修改后的值刷新到主内存,并且其他线程在读取这个变量时,会直接从主内存获取最新的值,而不是使用自己工作内存中的旧副本

这里我们使用一个例子来进行讲解:

public class VolatileDemo {private volatile boolean flag = false;public void setFlag(boolean flag) {this.flag = flag;}public boolean isFlag() {return flag;}
}

        在上述代码中,flag 变量被 volatile 修饰。当一个线程调用 setFlag 方法修改 flag 的值时,其他线程能够立即察觉到这个修改。

public class VolatileExample {public static void main(String[] args) {VolatileDemo volatileDemo = new VolatileDemo();Thread t1 = new Thread(() -> {while (!volatileDemo.isFlag()) {// 线程t1在此处循环等待,直到flag的值变为true}System.out.println("t1线程检测到flag为true,结束循环");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}volatileDemo.setFlag(true);System.out.println("t2线程将flag设置为true");});t1.start();t2.start();}
}

        在这个例子中,t1 线程在一个循环中不断检查 volatileDemo.flag 的值,如果为 false,则继续循环等待;t2 线程在睡眠1秒后将 flag 设置为 true。由于 flag 被 volatile 修饰,当 t2 线程修改 flag 的值后,t1 线程能够立即看到这个修改,从而结束循环。

        这样我们就了解了volatile关键字了。


以上就是本篇文章的全部内容了~~~

相关文章:

Java中的线程安全问题(如果想知道Java中有关线程安全问题的基本知识,那么只看这一篇就足够了!)

前言&#xff1a;多线程编程已经广泛开始使用&#xff0c;其可以充分利用系统资源来提升效率&#xff0c;但是线程安全问题也随之出现&#xff0c;它直接影响了程序的正确性和稳定性&#xff0c;需要对其进行深入的理解与解决。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解…...

基础设施即代码(IaC)在Python自动化运维中的应用探讨

基础设施即代码&#xff08;IaC&#xff09;在Python自动化运维中的应用探讨 目录 &#x1f310; IaC概念与工具介绍&#x1f40d; 使用Python实现基础设施自动化&#x1f4e6; 版本控制与基础设施管理的最佳实践&#x1f504; 部署环境的一致性与可复现性 1. &#x1f310; …...

浅谈路由器

路由器是一种网络设备&#xff0c;它在网络中起着至关重要的作用&#xff0c;主要功能包括&#xff1a; 1、数据转发&#xff1a;路由器的主要任务是将数据包从一个网络转发到另一个网络。它根据数据包的目的地址来决定将数据包发送到哪个网络。 2、路径选择&#xff1a;路由器…...

openGauss数据库-头歌实验1-1 初识openGauss

一、历史与特性 任务描述 本关任务&#xff1a;了解openGauss的发展历史以及相关特性。 相关知识 为了完成本关任务&#xff0c;你需要掌握&#xff1a;1.openGauss的发展历程&#xff0c;2.openGauss的功能特性。 发展历程 2019年9月19日在华为全联接大会上&#xff0c;…...

QT找不到ffmpeg链接库解决方法

error: undefined reference to avformat_network_init() 一个神奇的报错&#xff0c;查了很久&#xff0c;检查步骤&#xff1a; 1、检查了 pro工程文件 2、链接库的真实性和正确性 在main.cpp中调用没有报错&#xff0c;在其它cpp文件中调用就报错。 破案了&#xff0c;…...

消息队列-Rabbitmq(消息发送,消息接收)

将来我们开发业务功能的时候&#xff0c;肯定不会在控制台收发消息&#xff0c;而是应该基于编程的方式。由于RabbitMQ采用了AMQP协议&#xff0c;因此它具备跨语言的特性。任何语言只要遵循AMQP协议收发消息&#xff0c;都可以与RabbitMQ交互。并且RabbitMQ官方也提供了各种不…...

2、顶点着色器之视图矩阵

1、作用&#xff1a;将物体从世界坐标系转换到相机坐标系&#xff0c;相当于从世界坐标系转换到相机的局部(本地)坐标系。 2、基于LookAt函数的视图矩阵&#xff1a; 相机位置eye&#xff1a;(ex,ey,ez)&#xff0c;世界坐标系下的位置 目标位置center&#xff1a;(cx,cy,cz…...

crontab实现2026年开始每个月1号执行一次

要在 crontab 中设置一个任务&#xff0c;使其从 2026 年开始每个月的 1 号执行一次&#xff0c;可以使用以下格式&#xff1a; 0 0 1 * * <你的命令>这条规则的解释如下&#xff1a; 0 0&#xff1a;表示在每个月的 1 号的零点&#xff08;00:00&#xff09;执行。1&a…...

计算机网络803-(5)运输层

目录 一.运输层的两个主要协议&#xff1a;TCP 与 UDP 1.TCP/IP 的运输层有两个不同的协议&#xff1a; 2.端口号(protocol port number) &#xff08;1&#xff09;软件端口与硬件端口 &#xff08;2&#xff09;TCP 的端口 &#xff08;3&#xff09;三类端口 二.用户…...

八 MyBatis中接口代理机制及使用

八、MyBatis中接口代理机制及使用 实际上&#xff0c;第七章所讲内容mybatis内部已经实现了。直接调用以下代码即可获取dao接口的代理类&#xff1a; AccountDao accountDao (AccountDao)sqlSession.getMapper(AccountDao.class);使用以上代码的前提是&#xff1a;AccountMa…...

【解决】Ubuntu18.04 卸载python之后桌面异常且终端无法打开,重启后进入tty1,没有图形化界面

我因为python版本太过于混乱 &#xff08;都是为了学习os&#xff09; &#xff0c;3.6—3.9版本我都安装了&#xff0c;指向关系也很混乱&#xff0c;本着“重装是最不会乱”的原则&#xff0c;我把全部版本都卸载了。然后装了3.9 发现终端打不开了&#xff0c;火狐浏览器的图…...

OpenEmbedded、yocto和poky是什么关系?

Yocto项目是基于OpenEmbedded构建系统发展而来的。Yocto采用了OpenEmbedded的许多核心概念和工具&#xff0c;比如BitBake构建工具。BitBake在这两个系统中都是用于解析和处理recipes文件&#xff0c;这些recipes文件包含了软件包构建的指令、依赖关系、安装步骤等内容。 它们…...

记录页面——一个蛮好看的登录页(uni-app)

效果图 <template><view class"container"><view class"flex-col login-box"><view class"flex-col" style"width: 80%"><view class"flex-col"><text class"welcome-text-font&qu…...

Android文件选择器[超级轻量级FilePicker测试没有问题][挣扎解决自带文件管理器获取不到绝对地址问题而是返回msf%3A1000038197]

超级轻量级FilePicker测试没有问题 本文摘录于&#xff1a;https://blog.csdn.net/gitblog_00365/article/details/141449437只是做学习备份之用&#xff0c;绝无抄袭之意&#xff0c;有疑惑请联系本人&#xff01; 今天真的是发了疯的找文件管理器,因为调用系统自带的文件管理…...

【论文速读】| RED QUEEN: 保护大语言模型免受隐蔽多轮越狱攻击

基本信息 原文标题&#xff1a;RED QUEEN: Safeguarding Large Language Models against Concealed Multi-Turn Jailbreaking 原文作者&#xff1a;Yifan Jiang, Kriti Aggarwal, Tanmay Laud, Kashif Munir, Jay Pujara, Subhabrata Mukherjee 作者单位&#xff1a;Hippocr…...

39.第二阶段x86游戏实战2-HOOK实现主线程调用

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…...

wordpress argon主题美化方面

1、页面前端额外CSS&#xff1a; /*字体*/ font-face {font-family: myFont1; src:url(https://blog.yangmumu.com/css/fonts/Dancing.ttf) ;font-display: swap; } font-face {font-family: myFont2; src:url(https://blog.yangmumu.com/css/fonts/Regular.ttf) ;font-displa…...

qt QRadioButton详解

QRadioButton 是一个可以切换选中&#xff08;checked&#xff09;或未选中&#xff08;unchecked&#xff09;状态的选项按钮。单选按钮通常呈现给用户一个“多选一”的选择&#xff0c;即在一组单选按钮中&#xff0c;一次只能选中一个按钮。 重要方法 QRadioButton(QWidget…...

Qt 最小化,最大化,关闭窗口

Qt 最小化,最大化 在Qt中&#xff0c;你可以使用QWidget类提供的方法来实现窗口的最小化、最大化等操作。 最小化窗口 你可以使用QWidget的showMinimized()方法来最小化窗口。这将隐藏窗口并将其显示为系统托盘区域的图标。 connect(ui->btnMin,&QPushButton::click…...

【vue项目中添加告警音频提示音】

一、前提&#xff1a; 由于浏览器限制不能自动触发音频文件播放&#xff0c;所以实现此类功能时&#xff0c;需要添加触发事件&#xff0c;举例如下&#xff1a; 1、页面添加打开告警声音开关按钮 2、首次进入页面时添加交互弹窗提示&#xff1a;是否允许播放音频 以上两种方…...

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…...

树莓派超全系列教程文档--(61)树莓派摄像头高级使用方法

树莓派摄像头高级使用方法 配置通过调谐文件来调整相机行为 使用多个摄像头安装 libcam 和 rpicam-apps依赖关系开发包 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 配置 大多数用例自动工作&#xff0c;无需更改相机配置。但是&#xff0c;一…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

QMC5883L的驱动

简介 本篇文章的代码已经上传到了github上面&#xff0c;开源代码 作为一个电子罗盘模块&#xff0c;我们可以通过I2C从中获取偏航角yaw&#xff0c;相对于六轴陀螺仪的yaw&#xff0c;qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件

在选煤厂、化工厂、钢铁厂等过程生产型企业&#xff0c;其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进&#xff0c;需提前预防假检、错检、漏检&#xff0c;推动智慧生产运维系统数据的流动和现场赋能应用。同时&#xff0c;…...

《通信之道——从微积分到 5G》读书总结

第1章 绪 论 1.1 这是一本什么样的书 通信技术&#xff0c;说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号&#xff08;调制&#xff09; 把信息从信号中抽取出来&am…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

聊一聊接口测试的意义有哪些?

目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开&#xff0c;首…...

Angular微前端架构:Module Federation + ngx-build-plus (Webpack)

以下是一个完整的 Angular 微前端示例&#xff0c;其中使用的是 Module Federation 和 npx-build-plus 实现了主应用&#xff08;Shell&#xff09;与子应用&#xff08;Remote&#xff09;的集成。 &#x1f6e0;️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...