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

【JavaEE初阶系列】——带你了解volatile关键字以及wait()和notify()两方法背后的原理

目录

🚩volatile关键字

🎈volatile 不保证原子性

🎈synchronized 也能保证内存可见性

🎈Volatile与Synchronized比较

🚩wait和notify

🎈wait()方法

💻wait(参数)方法

🎈notify()方法

🎈解决线程饿死方式

🎈notifyAll()方法


🚩volatile关键字

volatile修饰的变量 能保证内存可见性
在学习Java多线程编程Q里, volatile 关键字 保证内存可见性的要点时,看到网上有些资料是这么说的: 线程修改一个变量,会把这个变量先从主内存读取到工作内存;然后修改工作内存中的值,最后再写回到主内存。
 
内存可见性问题 的表述为: t1 频繁读取主内存(内存),效率比较低,就被优化成直接读自己的工作内存(cpu寄存器);t2 修改了主内存的结果,但由于 t1 没有读主内存,导致修改不能被识别到,最终导致代码出现bug。
计算机运行的程序/代码,经常要访问数据。
这些依赖的数据 往往会存储在内存中去~(定义一个变量,变量就是在内存中)
  • cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中。再参与运算(load) 
  • cpu读取内存的这个操作,其实非常慢!(快,慢 都是相对的)
  • cpu进行大部分操作,都很快,一旦操作到读/写内存,此时的速度就降下来了。
  • 读内存 >> 读硬盘 快几千倍,上万倍
  • 读寄存器 >> 读内存  快几千倍,上万倍
结论:为了解决上述的问题,提高效率,此时编译器,就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器。减少读内存的次数,也就可以提高整体程序的效率了。

此时我们进行下面的代码段,首先我们默认isQuit是0,t2线程输入isQuit的值,t1线程中如果isQuit一直都是0的话,一直死循环,如果isQuit !=0的时候,我们才判断t1线程结束。

public class test1 {private static int isQuit=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (isQuit==0){}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}

此时我输入isQuit是1,然后t1线程理想的结果是跳出循环,然后输出t1线程退出。

但是,当我真正输入1的时候,此时t1线程并没有结束,t1线程正在执行,并且是RUNNABLE状态。很明显,实际效果和预期效果是不一样的,由于多线程引起的,也是线程安全的问题。之前是俩个线程同时修改同一个变量,现在是一个线程读,一个线程修改,也可能出现问题。此处 的问题,就是"内存可见性"情况引起的。

  • 1> load读取内存中的isQuit的值到寄存器中
  • 2>通过cmp指令比较寄存器的值是否是0,决定是否要继续循环

因为读寄存器的速度>>读内存的速度,所以短时间内,就会进行大量的循环,也就是进行大量的load和cmp操作。此时,编译器jvm就发现了,虽然进行了这么多次load但是load出现的结果都是一样的,并且load操作又非常的消耗时间,一次load花的时间相当于上万次的cmp了。所以编译器就做出了优化,只是第一次循环的时候,才读内存,后面都不再读内存了,而是直接从寄存器中,取出isQuit即可。

原本是load读取到内存中到寄存器中,然后cmp指令在寄存器中比较,依次来,但是由于cmp指令速度太快了大于load操作。编译器的初心是好的,它是希望提高程序的效率,但是提高效率的前提是保证逻辑不变。此时由于修改isQuit代码是另一个线程的操作,编译器没有正确的判定,所以编译器以为没人修改isQuit,就做出了上述优化,也就进一步引起了bug了。

后续 t2线程修改isQuit之后,t1感知不到isQuit变量的变化(感知不到内存的变化),所以一直比较,一直死循环。

解决上述 这个问题,volatile就是解决方案,在多线程的环境下,编译器对于是否要进行这样的优化,判定不一定准。就需要程序员通过volatile关键字告诉编译器,你不要优化(优化是算的快,但是算不准)。

这也告诉我们编译器也不是万能的,也会有一些短板的地方,此时就需要程序员进行补充了。只需要给isQuit加volatile关键字修饰,此时编译器自然就会禁止上述优化过程。

此时,程序就可以顺利退出了。


但是还有一种方式,就是让cmp指令比较的速度变慢,让处于休眠状态,这时候,load操作的开销就不大了,优化就没必要了。

public class test1 {private static int isQuit=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (isQuit==0){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}

但是我们编译器什么时候对其进行优化这是说不清楚的事情,所以用volatile修饰是最靠谱的事情。


🎈volatile 不保证原子性

volatile synchronized 有着本质的区别 . synchronized 能够保证原子性 , volatile 保证的是内存可见性. 代码示例
这个是最初的演示线程安全的代码 .
  • increase 方法去掉 synchronized
  • count 加上 volatile 关键字.
class Counter{volatile public int count = 0;void increase() {count++;}
}public  class Test2 {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 50000; i++) {counter.increase();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.count);}}

此时的结果依旧不是1w。所以可以证明volatile是不能保证原子性的。


🎈synchronized 也能保证内存可见性

synchronized 既能保证原子性 , 也能保证内存可见性 . 对上面的代码进行调整:
  • 去掉 isQuit  volatile
  • t1 的循环内部加上 synchronized, 并借助object对象加锁.
public class Test3 {private static  int isQuit=0;public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{while (true){synchronized (object){if(isQuit!=0){break;}}}System.out.println("t1线程退出");});Thread t2=new Thread(()->{System.out.println("请输入isQuit: ");Scanner scanner=new Scanner(System.in);//一旦用户输入的值,不为0,此时就会使t1线程执行结束isQuit=scanner.nextInt();});t1.start();t2.start();}
}


🎈Volatile与Synchronized比较

  • Volatile是轻量级的synchronized,因为它不会引起上下文的切换和调度,所以Volatile性能更好。
  • Volatile只能修饰变量,synchronized可以修饰方法,静态方法,代码块。
  • Volatile对任意单个变量的读/写具有原子性,但是类似于i++这种复合操作不具有原子性。而锁的互斥执行的特性可以确保对整个临界区代码执行具有原子性。
  • 多线程访问volatile不会发生阻塞,而synchronized会发生阻塞。
  • volatile是变量在多线程之间的可见性,synchronize是多线程之间访问资源的同步性。

🚩wait和notify

我们之前学的join方法,它是让一个线程执行完之后,再执行另一个线程,这就是哪个线程调用了join,哪个线程就阻塞。

join控制的是结束的先后顺序,但是理想情况下,是希望在结束前,先后顺序的控制。由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。

就比如在打篮球的时候,球场上每个运动员都是独立的"执行流",可以认为是一个"线程“,而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1先"传球",线程2才能"扣球",然后线程1就等待时机。阻塞等待又被唤醒继续执行,这种操作就是再一直的执行程序,而不是先完成一个线程之后第二线程再完成,然后就结束了。

比如,t1和t2两个线程,希望t1先执行,执行的差不多了,在让t2来干.就可以让t2先wait(阻塞,主动放弃cpu),等t1执行的差不多了,再通过notify来通知t2,把t2唤醒,让t2接着干.

  • 使用join,则必须让t1彻底执行完,t2才能运行.如果是希望t1先干50%的活,就让t2开始行动,此时join无能为力.
  • 使用sleep,指定一个休眠的时间.但是t1执行完这些代码,到底花了多少时间,不好估计.
  • 使用wait和notify可以更好的解决上述的问题.

🎈wait()方法

wait进行阻塞, 某个线程调用wait方法,就会进入阻塞(无论是通过哪个对象wait的),此时就处在WAITING状态.

wait,notify和notifyAll这几个类都是Object类的方法,所以Java里随便一个对象,都可以有这三种方法.

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法.

注意,wait也需要这个异常,这个异常,很多带有阻塞功能的方法都带.这些方法都是可以被interrupt方法通过这个异常给唤醒的。后续会再阻塞队列中讲到。

此时抛出异常, 非法的监视器状态异常。监视器是synchronized。
我们首先要知道 wait在执行的时候要进行三步骤:
  • 1.释放当前的锁
  • 2.让线程进入阻塞
  • 3.当线程被唤醒的时候,重新获取到锁
但是首先我们在执行这段代码的时候,我们是释放谁的锁呢?synchronized加锁其实就是把对象头的标记进行操作了, 释放锁的前提是加锁。就比如找工作,你再学校中,学校不让我出去找工作,所以我就不找了,但是前提是你得找到工作了,你才有选择去不去的选择,没有拿到offer之前就想着拒绝去。还比如,一个男生追一个女生,还没追到手都想到了以后和他在一起后孩子的名字都想好了,前提是你得追到手啊,追不到手你取再多名字都不行。
所以我们要让wait放进synchronized锁里面调用,这样就可以确保wait拿到了锁,你才有释放锁的能力。
public class wait_notify_test {public static void main(String[] args) throws InterruptedException {Object object=new Object();synchronized (object){System.out.println("wait之前");// 将wait放到synchronized里面调用,保证确实拿到了这个锁,才能释放锁object.wait();System.out.println("wait之后");}}
}

此时没有报错现象,打印了wait之前代码后,调用wait之后,程序就进入了阻塞状态,因为wait()这种方法无参的是保持死等待的 ,只有等到notify()唤醒才可以执行wait()方法后的程序。
wait 结束等待的条件:
  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

💻wait(参数)方法

wait除了默认的无参数版本之外,还有一个带参数的版本。带参数的版本就是指定超时时间,避免wait无休止的等待时间,等到一定的时间,就不会再等待了。

public class notify_wait_test2 {public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{synchronized (object){System.out.println("wait之前");try {object.wait(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait之后");}});t1.start();}
}


🎈notify()方法

我们设计下面的代码,线程t1进行wait(),线程t2进行唤醒wait(),因为wait的唤醒需要其他线程调用该对象的notify方法.首先t2线程睡眠3s,让ti线程阻塞等待一会,之后notify()唤醒了wait(),就开始进行wait()方法后的程序了。

public class notify_wait_test2 {public static void main(String[] args) {Object object=new Object();Thread t1=new Thread(()->{synchronized (object){System.out.println("wait之前");try {object.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("wait之后");}});Thread t2=new Thread(()->{try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (object){System.out.println("进行通知之前");object.notify();}});t1.start();t2.start();}
}


🎈解决线程饿死方式

就拿ATM机来举例子,1号滑稽进去之后,就要取钱,发现ATM里面没钱了,取不了,当1号滑稽释放锁之后,此时其他滑稽开始尝试竞争这个锁,但是刚才的1号滑稽,也能参与竞争这个锁。

所以每次都是1号滑稽进去之后,取不了钱,然后又进去,又取不了钱,又进去,其他线程等待锁,都是阻塞状态,没在cpu上执行,当1号滑稽释放锁之后,这些滑稽想去cpu,还需要有一个系统调度的过程,而1号自身,已经在cpu上执行,没有这个调度的过程了,1号近水楼台先得月,更容易拿到锁得。这就导致了一直是1号滑稽进入ATM机中,循环此处,每次都是取不了钱,但是还是1号滑稽占用了这个线程,这样长此以往就形成了”线程饿死“的状态。

针对上述情况,同样可以使用wait和notify解决,让1号滑稽,在发现没钱的时候,就进行wait(wait内部本身就会释放锁,并且进入阻塞),1号滑稽就不会参与后续的锁竞争了,也把锁释放出来让别人获取。就给其他的滑稽提供了机会了。
wait的过程是等,等待运钞车把钱送过来,运钞车的线程就相当于调用了notify唤醒的线程,这个等的过程,是阻塞的,但是不会占据cpu。

🎈notifyAll()方法

        notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程. 范例:使用notifyAll() 方法唤醒所有等待线程 , 在上面的代码基础上做出修改
调用wait不一定就只有一个线程调用,N个线程都可以调用wait,此时,当有多个线程调用的时候,这些线程都会进入阻塞状态。

        唤醒的方式就有2种方法。notifyAll唤醒的时候,wait要涉及到一个重新获取锁的过程,也是需要串行执行的而并不是并行执行。虽然提供了notifyAll,相比之下notify更可控,用的更多一些。


🚩wait 和 sleep 的对比(面试题)

其实理论上 wait sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间。
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
  • 1. wait 需要搭配 synchronized 使用. sleep 不需要.
  • 2. wait Object 的方法 sleep Thread 的静态方法

人拥有可以反复尝试的自由,也拥有停步或者回头的权利。

相关文章:

【JavaEE初阶系列】——带你了解volatile关键字以及wait()和notify()两方法背后的原理

目录 &#x1f6a9;volatile关键字 &#x1f388;volatile 不保证原子性 &#x1f388;synchronized 也能保证内存可见性 &#x1f388;Volatile与Synchronized比较 &#x1f6a9;wait和notify &#x1f388;wait()方法 &#x1f4bb;wait(参数)方法 &#x1f388;noti…...

GitHub配置SSH Key(详细版本)

GitHub配置SSH Key的目的是为了帮助我们在通过git提交代码是&#xff0c;不需要繁琐的验证过程&#xff0c;简化操作流程。比如新建的仓库可以下载, 但是提交需要账号密码。 步骤 一、设置git的user name和email 如果你是第一次使用&#xff0c;或者还没有配置过的话需要操作…...

JavaScript 权威指南第七版(GPT 重译)(六)

第十五章&#xff1a;JavaScript 在 Web 浏览器中 JavaScript 语言是在 1994 年创建的&#xff0c;旨在使 Web 浏览器显示的文档具有动态行为。自那时以来&#xff0c;该语言已经发生了显著的演变&#xff0c;与此同时&#xff0c;Web 平台的范围和功能也迅速增长。今天&#…...

Learning to summarize from human feedback

Abstract 人工参考总结以及 ROUGE 指标只是我们真实关心的目标(总结质量)的粗略代表。通过优化人工偏好来显著提升总结质量使用大量高质量的人类比较来训练一个模型来预测人类偏好的总结使用这个模型作为奖励函数对总结策略进行强化学习微调我们模型的效果在 TL;DR 数据集上显…...

数据库迁移测试

数据迁移测试 在进行项目重构或者更新的时候或多或少会对数据库进行变更&#xff0c;为了保证业务的稳定性对数据进行迁移测试是很有必要的&#xff0c;因为数据就是业务的基石&#xff0c;没有数据业务都是空中楼阁&#xff0c;形同虚设&#xff0c;小编结合近期的工作对数据…...

ASP .Net Core ILogger日志服务

&#x1f433;简介 ILogger日志服务是.NET平台中的一个内置服务&#xff0c;主要用于应用程序的日志记录。它提供了灵活的日志记录机制&#xff0c;允许开发者在应用程序中轻松地添加日志功能。以下是其主要特点和组件&#xff1a; ILogger接口&#xff1a;这是ILogger日志服…...

LeetCode 2657.找到两个数组的前缀公共数组

给你两个下标从 0 开始长度为 n 的整数排列 A 和 B 。 A 和 B 的 前缀公共数组 定义为数组 C &#xff0c;其中 C[i] 是数组 A 和 B 到下标为 i 之前公共元素的数目。 请你返回 A 和 B 的 前缀公共数组 。 如果一个长度为 n 的数组包含 1 到 n 的元素恰好一次&#xff0c;我…...

【jvm】jinfo使用

jinfo介绍 jinfo 是一个命令行工具&#xff0c;用于查看和修改 Java 虚拟机&#xff08;JVM&#xff09;的配置参数。它通常用于调试和性能调优。 使用 jinfo 命令&#xff0c;你可以查看当前 JVM 的配置参数&#xff0c;包括堆大小、线程数、垃圾回收器类型等。此外&#xf…...

C++ Thread 源码 观后 自我感悟 整理

Thread的主要数据成员为_Thr 里面存储的是线程句柄和线程ID 先看看赋值运算符的移动构造 最开始判断线程的ID是否不为0 _STD就是使用std的域 如果线程ID不为0&#xff0c;那么就抛出异常 这里_New_val使用了完美转发&#xff0c;交换_Val和_New_val的值 _Thr _STD exchange(_…...

2024阿里云2核2G服务器租用价格99元和61元一年

阿里云2核2G服务器配置优惠价格61元一年和99元一年&#xff0c;61元是轻量应用服务器2核2G3M带宽、50G高效云盘&#xff1b;99元服务器是ECS云服务器经济型e实例ecs.e-c1m1.large&#xff0c;2核2G、3M固定带宽、40G ESSD entry系统盘&#xff0c;阿里云活动链接 aliyunfuwuqi.…...

刚刚!奥特曼剧透GPT-5,将在高级推理功能上实现重大进步

奥特曼&#xff1a;“GPT-5的能力提升幅度将超乎人们的想象…” 自 Claude 3 发布以来&#xff0c;外界对 GPT-5 的期待越来越强。毕竟Claude 3已经全面超越了 GPT-4&#xff0c;成为迄今为止最强大模型。 而且距离 GPT-4 发布已经过去了整整一年时间&#xff0c;2023年3月14…...

uniapp使用Canvas给图片加水印把临时文件上传到服务器

生成的临时路径是没有完整的路径没办法上传到服务器 16:37:40.993 添加水印后的路径, _doc/uniapp_temp_1710923708347/canvas/17109238597881.png 16:37:41.041 添加水印后的完整路径, file://storage/emulated/0/Android/data/com.jingruan.zjd/apps/__UNI__BE4B000/doc/…...

小希的迷宫

目录 描述 输入 输出 样例输入 样例~~输出~~ 思路 code 描述 Gardon的迷宫城堡小希玩了很久&#xff08;见Problem B&#xff09;&#xff0c;现在她也想设计一个迷宫让Gardon来走。但是她设计迷宫的思路不一样&#xff0c;首先她认为所有的通道都应该是双向连通的&…...

MySQL索引剖析【了解背后的数据结构】

文章目录 常用索引概念聚簇索引 &#x1f389;非聚簇索引&#xff08;二级索引&#xff09; 数据结构选择Hash结构 ⭐️有序数组二叉搜索树AVL树&#xff08;平衡二叉搜索树&#xff09;B-Tree&#xff08;多路平衡查找树&#xff09;BTree ⭐️ MySQL中索引的实现InnoDB 索引实…...

004——内存映射(基于鸿蒙和I.MAX6ULL)

目录 一、 ARM架构内存映射模型 1.1 页表项 1.2 一级页表映射过程 1.3 二级页表映射过程 1.4 cache 和 buffer 二、 鸿蒙内存映射代码学习 三、 为板子编写内存映射代码 3.1 内存地址范围 3.2 设备地址范围 一、 ARM架构内存映射模型 &#xff08;以前我以为页表机制…...

150 Linux C++ 通讯架构实战6 服务器程序目录规划,makefile编写

从无到有产生这套 通讯架构源代码【项目/工程】 一&#xff0c;服务器程序目录规划 一个完整的项目 肯定会有多个源文件&#xff0c;头文件&#xff0c;会分别存放到多个目录&#xff1b; 我们这里要规划项目的目录结构&#xff1b; 注意&#xff1a;不固安是目录还是文件&am…...

OpenCV支持哪些类型的文件格式读写?

OpenCV支持多种类型的文件格式读写&#xff0c;包括但不限于以下格式&#xff1a; Windows位图文件&#xff1a;包括BMP和DIB格式。JPEG文件&#xff1a;支持JPEG、JPG和JPE三种扩展名。便携式网络图片&#xff1a;即PNG格式。便携式图像格式&#xff1a;包括PBM、PGM和PPM三种…...

数据库中使用IN操作效率问题

1. IN操作的基本概念 IN操作符在SQL中用于指定某个字段的值是否匹配列表中的任何值。这是一个条件操作符&#xff0c;用于在WHERE子句中过滤记录。 SQL语法示例&#xff1a; SELECT * FROM table_name WHERE column_name IN (value1, value2, ...); 2. IN操作的效率问题 当…...

unity学习(67)——控制器Joystick Pack方向

1.轮盘直接复制一个拖到右边就ok了&#xff0c;轮盘上是有脚本的。&#xff08;只复制&#xff09; 2.上面的显示窗也可以复制&#xff0c;但是要绑定对应的轮盘&#xff08;unity中修改变量&#xff09;&#xff0c;显示窗上是有脚本的。&#xff08;复制改变量&#xff09; 3…...

MATLAB的使用(一)

一&#xff0c;MATLAB的编程特点 a,语法高度简化&#xff1b; b,脚本式解释型语言&#xff1b; c,针对矩阵的高性能运算&#xff1b; d,丰富的函数工具箱支持&#xff1b; e,通过matlab本体构建跨平台&#xff1b; 二&#xff0c;MATLAB的界面 工具栏:提供快捷操作编辑器…...

R语言AI模型部署方案:精准离线运行详解

R语言AI模型部署方案:精准离线运行详解 一、项目概述 本文将构建一个完整的R语言AI部署解决方案,实现鸢尾花分类模型的训练、保存、离线部署和预测功能。核心特点: 100%离线运行能力自包含环境依赖生产级错误处理跨平台兼容性模型版本管理# 文件结构说明 Iris_AI_Deployme…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

Android15默认授权浮窗权限

我们经常有那种需求&#xff0c;客户需要定制的apk集成在ROM中&#xff0c;并且默认授予其【显示在其他应用的上层】权限&#xff0c;也就是我们常说的浮窗权限&#xff0c;那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...

如何在最短时间内提升打ctf(web)的水平?

刚刚刷完2遍 bugku 的 web 题&#xff0c;前来答题。 每个人对刷题理解是不同&#xff0c;有的人是看了writeup就等于刷了&#xff0c;有的人是收藏了writeup就等于刷了&#xff0c;有的人是跟着writeup做了一遍就等于刷了&#xff0c;还有的人是独立思考做了一遍就等于刷了。…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

智能分布式爬虫的数据处理流水线优化:基于深度强化学习的数据质量控制

在数字化浪潮席卷全球的今天&#xff0c;数据已成为企业和研究机构的核心资产。智能分布式爬虫作为高效的数据采集工具&#xff0c;在大规模数据获取中发挥着关键作用。然而&#xff0c;传统的数据处理流水线在面对复杂多变的网络环境和海量异构数据时&#xff0c;常出现数据质…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...

WebRTC从入门到实践 - 零基础教程

WebRTC从入门到实践 - 零基础教程 目录 WebRTC简介 基础概念 工作原理 开发环境搭建 基础实践 三个实战案例 常见问题解答 1. WebRTC简介 1.1 什么是WebRTC&#xff1f; WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个支持网页浏览器进行实时语音…...

鸿蒙(HarmonyOS5)实现跳一跳小游戏

下面我将介绍如何使用鸿蒙的ArkUI框架&#xff0c;实现一个简单的跳一跳小游戏。 1. 项目结构 src/main/ets/ ├── MainAbility │ ├── pages │ │ ├── Index.ets // 主页面 │ │ └── GamePage.ets // 游戏页面 │ └── model │ …...

Visual Studio Code 扩展

Visual Studio Code 扩展 change-case 大小写转换EmmyLua for VSCode 调试插件Bookmarks 书签 change-case 大小写转换 https://marketplace.visualstudio.com/items?itemNamewmaurer.change-case 选中单词后&#xff0c;命令 changeCase.commands 可预览转换效果 EmmyLua…...