3. Java中的锁

文章目录

  • 乐观锁与悲观锁
    • 乐观锁(无锁编程,版本号机制)
    • 悲观锁
    • 两种锁的伪代码比较
  • 通过 8 种锁运行案例,了解锁
    • 锁相关的 8 种案例演示
      • 场景一
      • 场景二
      • 场景三
      • 场景四
      • 场景五
      • 场景六
      • 场景七
      • 场景八
    • synchronized 有三种应用方式
      • 8 种锁的案例实际体现在 3 个地方
    • 从字节码角度分析 synchronized 实现
      • `javap -c ****.class`文件反编译
      • synchronized 同步代码块
      • synchronized 普通同步方法
      • synchronized 静态同步方法
    • 对于 synchronized 的深入研究
      • 面试题:为什么任何一个对象都可以成为一个锁
      • 什么是管程 monitor
  • 公平锁与非公平锁
    • 为什么会有公平锁和非公平锁的设计?
    • 为什么默认使用非公平锁?
    • 什么时候用公平锁?什么时候用非公平锁?
  • 可重入锁,又叫,递归锁
    • 隐式锁(synchronized 默认是可重入锁)
      • 同步块
      • 同步方法
    • synchronized 的可重入原理(基于 objectMonitor.hpp)
    • 显示锁(Lock,ReentrantLock)
  • 死锁以及排查
    • 死锁是什么
    • 编写一个死锁 case
    • 死锁的故障排查
  • 小结
  • 见后续
    • 自旋锁 SpinLock
    • 无锁->独占锁->读写锁->邮戳锁
    • 无锁->偏向锁->轻量锁->重量锁

乐观锁与悲观锁

乐观锁(无锁编程,版本号机制)

  • 认为自己在使用数据时,不会有别的线程修改数据或资源,所以不会加锁 。
  • 在 Java 中通过使用无锁编程来实现,只在更新数据时去判断,之前是否存在其它线程更新此数据。
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
    • 如果这个线程数据已经被其他线程更新,则根据不同的实现方式执行不同的操作
      • 放弃修改,尝试抢锁等
  • 判断规则
    • 版本号机制 Version
    • 最常采用的是 CAS 算法,Java 原子类的递增操作就通过 CAS 自旋实现的
  • 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
  • 乐观锁直接去操作同步资源,是一种无锁算法
  • 乐观锁的两种实现方式
    • 采用 Version 版本号机制
    • CAS(Compare-and-Swap,比较替换算法) 实现

悲观锁

  • 认为自己在使用数据时,必然有别的线程来修改数据,因此在获取到数据的时候,进行操作之前,会先加锁,保证数据不被别的线程所修改
  • synchronized 关键字与 Lock 锁的实现类均为悲观锁
  • 适合写操作多的场景,先加锁可以保证写操作时数据正确,
  • 显示锁定之后再进行同步资源

两种锁的伪代码比较

  • 悲观锁
 //悲观锁基于synchronized关键字public synchronized void m1(){//code logic segment....}//悲观锁基于Lock对象实现ReentrantLock reentrantLock = new ReentrantLock();public void m2(){reentrantLock.lock();try {//code logic segment....} finally {reentrantLock.unlock();}}
  • 乐观锁
        //乐观锁的调用方式,保证多个线程使用的是同一个AtomicIntegerAtomicInteger atomicInteger = new AtomicInteger();atomicInteger.incrementAndGet();

通过 8 种锁运行案例,了解锁

锁相关的 8 种案例演示

场景一

a,b 两个线程分别使用同步监视器修饰
a 线程启动,0.2 秒后 b 线程启动
可以预见,先执行 a 后执行 b

  • 代码
class Phone {//资源类public synchronized void sendEmail() {System.out.println(Thread.currentThread().getName() + "-----sendEmail");}public synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
}...
public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
  • 效果

image.png

场景二

在场景一的资源类中,sendEmail方法中加入暂停3秒钟
由于 synchronized 是悲观锁,并且 sleep 不回释放锁,因此 a 线程先执行,并且执行 sleep 时程序也会阻塞,当 a 线程执行完毕时,b 线程才会执行
可以预见结果和场景一致

  • 代码
    public  synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "-----sendEmail");}
  • 效果

image.png

一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法

场景三

添加一个普通的hello方法,先打印邮件还是hello?
普通方法线程共享

class Phone {//资源类....public void hello() {System.out.println("-------hello");}....
}//main方法public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.hello();}, "b").start();}
  • 效果

image.png

场景四

有两部手机,请问先打印邮件还是短信
两次方法的调用 synchronized 锁住的对象不同

  • 代码
//资源类不变public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

场景五

在场景 一的情况下将两个方法均添加 static 修饰,测试代码同场景一
静态同步方法(类锁)

  • 代码
    public static synchronized void sendEmail() {try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "-----sendEmail");}public static synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
  • 效果

image.png

场景六

在场景五的情况下,添加一部手机,用 phone2 调用 sentSMS

  • 代码
//资源类同场景五
//测试代码public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——>实例对象本身(方法调用者),
对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板(调用者所属的类型)
对于同步方法块,锁的是 synchronized 括号内的对象

场景七

有1个静态同步方法,有1个普通同步方法,有1部手机,请问先打印邮件还是短信

    //将短信方法还原为普通同步方法public static synchronized void sendSMS() {System.out.println(Thread.currentThread().getName() + "-----sendSMS");}
  • 测试代码同场景一
    public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone.sendSMS();}, "b").start();}
  • 效果

image.png

类锁与对象锁不是同一个,各种执行,a 线程睡眠 3 秒

场景八

有1个静态同步方法,有1个普通同步方法,有2部手机,请问先打印邮件还是短信

//资源类同场景七
//测试代码public static void main(String[] args) {//一切程序的入口Phone phone = new Phone();Phone phone2 = new Phone();new Thread(() -> {phone.sendEmail();}, "a").start();//暂停毫秒,保证a线程先启动try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {phone2.sendSMS();}, "b").start();}
  • 效果

image.png

当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

synchronized 有三种应用方式

8 种锁的案例实际体现在 3 个地方

  • 作用于实例方法,为当前方法调用者加锁,进入同步代码前需要获得当前实例的锁
  • 作用于代码块,synchronized(obj){},obj 为加锁对象
  • 作用于静态方法,当前类加锁,进入同步代码块之前需要获取类对象的锁

从字节码角度分析 synchronized 实现

javap -c ****.class文件反编译

  • -c 作用: 对代码进行反编译
  • -v (verbose) 作用 输出附加信息(行号,本地变量表,反编译等详细信息)

synchronized 同步代码块

  • 编写测试代码->运行产生 class 文件->进入 class 类路径–>执行 javap -c
Object object = new Object();public void m1() {synchronized (object) {System.out.println("----hello synchronized code block");}}
  • 反编译结果
    • 一般情况下,一个 enter 对应 2 个 exit

image.png

  • 极端情况==>手动抛出一个异常
Object object = new Object();public void m1() {synchronized (object) {System.out.println("----hello synchronized code block");throw new RuntimeException("-----exp");}
}

image.png

  • synchronized 同步代码块的实现
    • 进入锁使用 monitorenter 指令
    • 退出锁使用 monitorexit 指令

synchronized 普通同步方法

    public synchronized void m2() {System.out.println("----hello synchronized m2");}
  • 使用javap -v .\LockSyncDemo.class进行编译

image.png

  • 调用指令时检查方法的 ACC_SYNCHRONIZED 访问标志位是否被设置
    • 若被设置了,则线程会将先持有 monitor 锁,然后再执行方法
    • 最后在方法完成时(无论正常完成还是非正常完成)均会释放 monitor

synchronized 静态同步方法

public static synchronized void m3() {System.out.println("----hello static synchronized m3");
}
  • 执行反编译

image.png

  • ACC_STATIC, ACC_SYNCHRONIZED 访问标志位区分该方法是否为静态同步方法

对于 synchronized 的深入研究

面试题:为什么任何一个对象都可以成为一个锁

什么是管程 monitor

  • HotSport 虚拟机中,monitor 采用 ObjectMonitor 实现
  • C++源码执行过程

  • Object 底层实现基于 ObjectMonitor.cpp,所有类默认继承自 Object 类,因此每个对象天生就带着一个 monitor 对象
  • 每一个锁住的对象均会与 monitor 进行关联
  • objectMonitor.hpp 中的源码片段
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {_header       = NULL;_count        = 0;			//记录该线程获取锁的次数_waiters      = 0,recursions   = 0;			//锁的重入次数_object       = NULL;_owner        = NULL;	    //指向持有ObjectMonitor对象的线程_WaitSet      = NULL;		//存放的处于wait状态的线程队列_WaitSetLock  = 0 ;			_Responsible  = NULL ;_succ         = NULL ;_cxq          = NULL ;FreeNext      = NULL ;_EntryList    = NULL ;		//存放处于等待锁block状态的线程队列_SpinFreq     = 0 ;_SpinClock    = 0 ;OwnerIsThread = 0 ;_previous_owner_tid = 0;
}

公平锁与非公平锁

公平锁是指多线程按照申请锁的顺序来获取锁,先来先得
Lock lock = new ReentrantLock(true); //true表示先来先得


非公平锁是指,获取锁的顺序不是按照申请锁的顺序
存在后申请的线程比先申请的线程优先获取锁
在高并发环境下,存在优先级翻转或者锁饥饿的状态
锁饥饿 : 某个线程长时间得不到锁
Lock lock = new ReentrantLock(false); //false 表示非公平锁,并发抢锁
Lock lock = new ReentrantLock(); //默认非公平锁

为什么会有公平锁和非公平锁的设计?

  • 恢复挂起的线程到真正锁的获取还是有时间差的
  • 对 CPU 而言时间差较为明细,非公平锁能够更充分利用 CPU 的时间片,减少 CPU 空闲时间

为什么默认使用非公平锁?

  • 使用多线程的一个考量点就是线程切换的开销
    • 采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,则刚释放锁的线程在此时再次获得同步状态的概率就变得非常大,因此减少了线程的开销

什么时候用公平锁?什么时候用非公平锁?

  • 为了提高系统的吞吐量,提升性能,减少不必要的时间开销,应选择非公平锁
  • 公平锁需要结合具体场景进行讨论

可重入锁,又叫,递归锁

同一个线程在外层方法获取锁的时候,再进入该线程都内层方法会自动获取锁(前提,锁的对象是同一个),不会因为之前已经获取过还没释放而阻塞

  • Java 中 synchronized 和 ReentrantLock 都是可重入锁
  • 可重入锁可以一定程度上避免死锁
  • 可重入锁,即可多次进入同步域==>同步代码块,同步方法,lock()与 unlock()包裹的区域

隐式锁(synchronized 默认是可重入锁)

  • 在一个 synchronized 修饰的方法或代码块内部调用本类的其它 synchronized 修饰的方法或代码块时,永远可以得到锁

同步块

    private static void reEntryM1() {final Object object = new Object();new Thread(() -> {synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----外层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----中层调用");synchronized (object) {System.out.println(Thread.currentThread().getName() + "\t ----内层调用");}}}}, "t1").start();}
  • 在 main 方法中进行调用,测试结果

image.png

同步方法

     public synchronized void m1() {//指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。System.out.println(Thread.currentThread().getName()+"\t ----come in");m2();System.out.println(Thread.currentThread().getName()+"\t ----end m1");}public synchronized void m2(){System.out.println(Thread.currentThread().getName()+"\t ----come in");m3();}public synchronized void m3(){System.out.println(Thread.currentThread().getName()+"\t ----come in");}//main方法调用public static void main(String[] args){ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();new Thread(() -> {reEntryLockDemo.m1();}, "t1").start();}
  • 效果

image.png

synchronized 的可重入原理(基于 objectMonitor.hpp)

  • 每个锁对象都拥有一个锁计数器和一个指向持有锁的线程指针
    • 当执行 monitorenter 时,如果目标锁的计数器为零,那么没有被其他线程所持有,JVM 会将该锁对象持有的线程设置为当前线程,并将计数器加 1
    • 在目标锁对象的计数器不为零的情况下,如果锁的持有线程是但前线程,那么 JVM 可以将其计数器加 1,否则进入等待,直至持有线程释放该锁
    • 当执行 monitorexit 时,JVM 将锁对象的计数器减 1,计数器为 0 表示锁已被释放

显示锁(Lock,ReentrantLock)

    new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");}finally {lock.unlock();}}finally {// 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。lock.unlock();// 正常情况,加锁几次就要解锁几次}},"t1").start();new Thread(() -> {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");}finally {lock.unlock();}},"t2").start();
  • 效果

image.png

  • lock()与 unlock()未一一匹对

image.png

  • 效果

image.png

  • t1 外层未释放锁,t2 陷入持续等待…

死锁以及排查

死锁是什么

参考往期文章

编写一个死锁 case

 public static void main(String[] args) {final Object objectA = new Object();final Object objectB = new Object();new Thread(() -> {synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");}}}, "A").start();new Thread(() -> {synchronized (objectB) {System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}synchronized (objectA) {System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");}}}, "B").start();}

死锁的故障排查

  • jps -l
  • jstack pid

image.png
image.png

  • jconsole

image.png

小结


见后续

自旋锁 SpinLock

无锁->独占锁->读写锁->邮戳锁

无锁->偏向锁->轻量锁->重量锁


在这里插入图片描述

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

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

相关文章

CentOS 7全系列免费

CentOS 7 全系列免费:桌面版、工作站版、服务器版等等………… 上文,关于CentOS 7这句话,被忽略了。 注意版本:知识产权、网络安全。

2024河北国际光伏展

2024河北国际光伏展是一个专门展示和促进光伏技术与产业发展的国际性展览会。该展览会将于2024年在中国河北省举办,吸引来自世界各地的光伏企业、专家、学者和投资者参加。 展览会将展示最新的光伏技术和产品,包括太阳能电池板、光伏组件、逆变器、储能系…

ChatGPT 国内快速上手指南

ChatGPT简介 ChatGPT是由OpenAI团队研发的自然语言处理模型,该模型在大量的互联网文本数据上进行了预训练,使其具备了深刻的语言理解和生成能力。 GPT拥有上亿个参数,这使得ChatGPT在处理各种语言任务时表现卓越。它的训练使得模型能够理解上…

2024年CSC博导短期出国交流项目指南、材料准备及问题解答

2024年国家留学基金委(CSC)继续实施博士生导师短期出国交流项目,知识人网小编仅转载该项目指南、申请材料及说明和常见问题解答,详情请咨询国家留学基金委。 2024年博士生导师短期出国交流项目指南 第一章 总则 第一条 为进一步…

如何把mp4音频转换成mp3?四招教你将MP4音频转为MP3格式

如何把mp4音频转换成mp3?在数字多媒体的世界里,音频和视频格式多种多样,每种格式都有其独特之处。其中,MP4和MP3是最常见的两种格式。MP4通常用于视频文件,而MP3则专用于音频。有时,我们可能希望将MP4文件中…

[算法沉淀记录] 排序算法 —— 堆排序

排序算法 —— 堆排序 算法基础介绍 堆排序(Heap Sort)是一种基于比较的排序算法,它利用堆这种数据结构来实现排序。堆是一种特殊的完全二叉树,其中每个节点的值都必须大于或等于(最大堆)或小于或等于&am…

【生成式AI】ChatGPT 原理解析(2/3)- 预训练 Pre-train

Hung-yi Lee 课件整理 预训练得到的模型我们叫自监督学习模型(Self-supervised Learning),也叫基石模型(foundation modle)。 文章目录 机器是怎么学习的ChatGPT里面的监督学习GPT-2GPT-3和GPT-3.5GPTChatGPT支持多语言…

设计模式浅析(九) ·模板方法模式

设计模式浅析(九) 模板方法模式 日常叨逼叨 java设计模式浅析,如果觉得对你有帮助,记得一键三连,谢谢各位观众老爷😁😁 模板方法模式 概念 模板方法模式(Template Method Pattern)在Java中是…

蓝桥杯STM32G431RBT6实现按键的单击、双击、长按的识别

阅读引言: 是这样, 我也参加了这个第十五届的蓝桥杯,查看竞赛提纲的时候发现有按键的双击识别, 接着我就自己实现了一个按键双击的识别,但是识别效果不是特别理想,偶尔会出现识别不准确的情况,接…

云原生之容器编排实践-ruoyi-cloud项目部署到K8S:MySQL8

背景 前面搭建好了 Kubernetes 集群与私有镜像仓库,终于要进入服务编排的实践环节了。本系列拿 ruoyi-cloud 项目进行练手,按照 MySQL , Nacos , Redis , Nginx , Gateway , Auth ,…

windows安装fay数字人

创建虚拟环境 conda create -p D:\CondaEnvs\paystu python3.9下载所需资料 源代码 数字形象 将源代码和虚拟象形解压 再源代码文件夹下激活虚拟环境 conda activate D:\CondaEnvs\paystu安装依赖包 注意 会安装pytorch # conda install --yes --file requirements.txt pi…

jupyter notebook闪退解决,安美解决

jupyter notebook闪退 解决方法 打开这个目录 删除含有“~”开头的文件 解决

centos升级g++.v6.1.0版本

1.下载源码包 wget http://ftp.gnu.org/gnu/gcc/gcc-6.1.0/gcc-6.1.0.tar.gz tar -zxvf gcc-6.1.0.tar.gz cd gcc-6.1.0 2.打开 download_prerequisites 脚本 vim contrib/download_prerequisites 可以看到该文件就是执行一些下载指令,需要下载几个包&#xff0c…

顺序表的列题(力扣)和旋转数组

文章目录 一.删除有序数组中的重复项(取自力扣) 二.合并两个有序数组(取自力扣) 三.旋转数组(多解法) 前言 见面我们说到了顺序表今天来分享几个有关于顺序表的题目 一.删除有序数组中的重复项&#xff…

数据结构2月21日

双向链表: func函数&#xff1a; #include <stdio.h> #include <stdlib.h> …

【Flink精讲】Flink性能调优:内存调优

内存调优 内存模型 JVM 特定内存 JVM 本身使用的内存&#xff0c;包含 JVM 的 metaspace 和 over-head 1&#xff09; JVM metaspace&#xff1a; JVM 元空间 taskmanager.memory.jvm-metaspace.size&#xff0c;默认 256mb 2&#xff09; JVM over-head 执行开销&#xff1…

【web】云导航项目部署及环境搭建(复杂)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、项目介绍1.1项目环境架构LNMP1.2项目代码说明 二、项目环境搭建2.1 Nginx安装2.2 php安装2.3 nginx配置和php配置2.3.1 修改nginx文件2.3.2 修改vim /etc/p…

LeetCode--134

134. 加油站 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发&#xff0c;开始时油箱为空。 给定两个整数数组 …

Camera metadata设计与应用

前言 Android的Camera Metadata是一种数据结构&#xff0c;用于表示图像特征的参数&#xff0c;例如常见的曝光、AE、AWB、flash等参数。在新的Camera API2 / HAL3架构中&#xff0c;这种结构被用来在app-hal之间IPC传输&#xff0c;取代了原先的SetParameter()/GetParameter(…

【蓝桥杯单片机入门记录】动态数码管

目录 一、数码管动态显示概述 二、动态数码管原理图 &#xff08;1&#xff09;原理图 &#xff08;2&#xff09;动态数码管如何与芯片相连 &#xff08;3&#xff09;“此器件” ——>锁存器74HC573 三、动态数码管显示例程 &#xff08;1&#xff09;例程1&#xf…