一文搞懂到底什么是 AQS

日常开发中,我们经常使用锁或者其他同步器来控制并发,那么它们的基础框架是什么呢?如何实现的同步功能呢?本文将详细讲解构建锁和同步器的基础框架--AQS,并根据源码分析其原理。


一、什么是 AQS?

(一) AQS 简介

AQS(Abstract Queued Synchronizer),抽象队列同步器,它是用来构建锁或其他同步器的基础框架。虽然大多数程序员可能永远不会使用到它,但是知道 AQS 的原理有助于理解一些锁或同步器的是如何运行的。

那么有哪些同步器是基于 AQS 实现的呢?这里仅是简单介绍,详情后续会单独总结一篇文章。

同步器

说明

CountDownLatch

递减的计数器,直至所有线程的任务都执行完毕,才继续执行后续任务。

Semaphore

信号量,控制同时访问某个资源的数量。

CyclicBarrier

递增的计数器,所有线程达到屏障时,才会继续执行后续任务。

ReentrantLock

防止多个线程同时访问共享资源,类似 synchronized 关键字。

ReentrantReadWriteLock

维护了读锁和写锁,读锁允许多线程访问,读锁阻塞所有线程。

Condition

提供类似 Object 监视器的方法,于 Lock 配合可以实现等待/通知模式。

FutureTask

当一个线程需要等待另一线程把某个任务执行完后才能继续执行,此时可以使用 FutureTask

如果你理解了 AQS 的原理,也可以基于它去自定义一个同步组件,下文会介绍。

(二) AQS 数据结构

AQS 核心是通过对同步状态的管理,来完成线程同步,底层是依赖一个双端队列来完成同步状态的管理

  • 当前线程获取同步状态失败后,会构造成一个 Node 节点并加入队列末尾,同实阻塞线程。
  • 当同步状态释放时,会把头节点中的线程唤醒,让其再次尝试获取同步状态

如下图,这里只是简单绘制,具体流程见下面原理分析:

这里的每个 Node 节点都存储着当前线程、等待信息等。

(三) 资源共享模式

我们在获取共享资源时,有两种模式:

模式

说明

示例

独占模式

Exclusive,资源同一时刻只能被一个线程获取

ReentrantLock

共享模式

Share,资源可同时被多个线程获取

Semaphore、CountDownLatch

二、AQS 原理分析

先简单说下原理分析的流程:

  1. 同步状态相关源码;
  2. 须重写的方法;
  3. Node 节点结构分析;
  4. 独占模式下的同步状态的获取与释放;
  5. 共享模式下的同步状态的获取与释放;

(一) 同步状态相关

上面介绍到, AQS 核心是通过对同步状态的管理,来完成线程同步,所以首先介绍管理同步状态的三个方法,在自定义同步组件时,需要通过它们获取和修改同步状态。

//保证可见性
private volatile int state//获取当前同步状态。
protected final int getState() {return state;
}//设置当前同步状态。
protected final void setState(int newState) {state = newState;
}//使用 CAS 设置当前状态,保证原子性。
protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

(二) 须重写的方法

AQS 是基于模板方法模式的,通过第一个 abstract 也可知道,AQS 是个抽象类,使用者需要继承 AQS 并重写指定方法。

以下这些方式是没有具体实现的,需要在使用 AQS 时在子类中去实现具体方法,等到介绍一些同步组件时,会详细说明如何重写。

//独占式获取同步状态,实现该方法须查询并判断当前状态是否符合预期,然后再进行CAS设置状态。
protected boolean tryAcquire (int arg) //独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态。
protected boolean tryRelease (int arg) //共享式获取同步状态,返回大于0的值表示获取成功,反之获取失败。
protected int tryAcquireShared (int arg)//共享式释放同步状态。
protected boolean tryReleaseShared (int arg)//当前同步器是否再独占模式下被线程占用,一般用来表示是否被当前线程独占。
protected boolean isHeldExclusively ()

(三) Node 源码

Node 是双端队列中的节点,是数据结构的重要部分,线程相关的信息都存在每一个 Node 中。

1. Node 结构源码

源码如下:

static final class Node {//标记当前节点的线程在共享模式下等待。static final Node SHARED = new Node();//标记当前节点的线程在独占模式下等待。static final Node EXCLUSIVE = null;//waitStatus的值,表示当前节点的线程已取消(等待超时或被中断)static final int CANCELLED =  1;//waitStatus的值,表示后继节点的线程需要被唤醒static final int SIGNAL    = -1;//waitStatus的值,表示当前节点在等待某个条件,正处于condition等待队列中static final int CONDITION = -2;//waitStatus的值,表示在当前有资源可用,能够执行后续的acquireShared操作static final int PROPAGATE = -3;//等待状态,值如上,1、-1、-2、-3。volatile int waitStatus;//前趋节点volatile Node prev;//后继节点volatile Node next;//当前线程volatile Thread thread;//等待队列中的后继节点,共享模式下值为SHARED常量Node nextWaiter;//判断共享模式的方法final boolean isShared() {return nextWaiter == SHARED;}//返回前趋节点,没有报NPEfinal Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}//下面是三个构造方法Node() {}    // Used to establish initial head or SHARED markeNode(Thread thread, Node mode) {     // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}
}

2. 设置头尾节点

Unsafe 类中,提供了一个基于 CAS 的设置头尾节点的方法,AQS 调用该方法进行设置头尾节点,保证并发编程中的线程安全。

//CAS自旋设置头节点
private final boolean compareAndSetHead(Node update) {return unsafe.compareAndSwapObject(this, headOffset, null, update);
}//CAS自旋设置尾节点,expect为当前线程“认为”的尾节点,update为当前节点
private final boolean compareAndSetTail(Node expect, Node update) {return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

(四) 独占模式

资源同一时刻只能被一个线程获取,如 ReentrantLock。

1. 获取同步状态

代码如下,调用 acquire 方法可以获取同步状态,底层就是调用须重写方法中的 tryAcquire。如果获取失败则进入同步队列中,即使后续对线程进行终端操作,线程也不会从同步队列中移除。

public final void acquire(int arg) {//调用须重写方法中的tryAcquireif (!tryAcquire(arg) &&//失败则进入同步队列中acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

获取失败会先调用 addWaiter 方法将当前线程封装成独占式模式的节点,添加到AQS的队列尾部,源码如下。

private Node addWaiter(Node mode) {//将当前线程封装成对应模式下的Node节点Node node = new Node(Thread.currentThread(), mode);Node pred = tail;//尾节点if (pred != null) {//双端队列需要两个指针指向node.prev = pred;//通过CAS方式if (compareAndSetTail(pred, node)) {//添加到队列尾部pred.next = node;return node;}}//等待队列中没有节点,或者添加队列尾部失败则调用end方法enq(node);return node;
}//Node节点通过CAS自旋的方式被添加到队列尾部,直到添加成功为止。
private Node enq(final Node node) {//死循环,类似 while(1)for (;;) {Node t = tail;if (t == null) { // 须要初始化,代表队列的第一个元素if (compareAndSetHead(new Node()))//头节点就是尾节点tail = head;} else {//双端队列需要两个指针指向node.prev = t;//通过自旋放入队列尾部if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

此时,通过 addWaiter 已经将当前线程封装成独占模式的 Node 节点,并成功放入队列尾部。接下来会调用acquireQueued 方法在等待队列中排队。

final boolean acquireQueued(final Node node, int arg) {//获取资源失败标识boolean failed = true;try {//线程是否被中断标识boolean interrupted = false;//死循环,类似 while(1)for (;;) {//获取当前节点的前趋节点final Node p = node.predecessor();//前趋节点是head,即队列的第二个节点,可以尝试获取资源if (p == head && tryAcquire(arg)) {//资源获取成功将当前节点设置为头节点setHead(node);p.next = null; // help GC,表示head节点出队列failed = false;return interrupted;}//判断当前线程是否可以进入waitting状态,详解见下方if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())	//阻塞当前线程,详解见下方interrupted = true;}} finally {if (failed)//取消获取同步状态,源码见下方的取消获取同步状态章节cancelAcquire(node);}
}//将当前节点设置为头节点
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}//判断当前线程是否可以进入waitting状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取前趋节点的等待状态,含义见上方Node结构源码int ws = pred.waitStatus;if (ws == Node.SIGNAL)	//表示当前节点的线程需要被唤醒return true;if (ws > 0) {	//表示当前节点的线程被取消//则当前节点一直向前移动,直到找到一个waitStatus状态小于或等于0的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);//排在这个节点的后面pred.next = node;} else {//通过CAS设置等待状态compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}//阻塞当前线程
private final boolean parkAndCheckInterrupt() {//底层调用的UnSafe类的方法 park:阻塞当前线程, unpark:使给定的线程停止阻塞LockSupport.park(this);//中断线程return Thread.interrupted();
}

acquireQueued 方法中,只有当前驱节点等于 head 节点时,才能够尝试获取同步状态,这时为什么呢?

因为 head 节点是占有资源的节点,它释放后才会唤醒它的后继节点,所以需要检测。还有一个原因是因为如果遇到了非 head 节点的其他节点出队或因中断而从等待中唤醒,这时种情况则需要判断前趋节点是否为 head 节点,是才允许获取同步状态。

获取同步状态的整体流程图如下:

2. 释放同步状态

调用须重写方法中的 tryAcquire 进行同步状态的释放,成功则唤醒队列中最前面的线程,具体如下。

public final boolean release(int arg) {//调用须重写方法中的tryReleaseif (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)//唤醒后继节点的线程,详情见下方unparkSuccessor(h);return true;}return false;
}//唤醒后继节点的线程
private void unparkSuccessor(Node node) {//获取当前节点的等待状态int ws = node.waitStatus;if (ws < 0)//小于0则,则尝试CAS设为0compareAndSetWaitStatus(node, ws, 0);//获取后继节点Node s = node.next;//后继节点为空或者等待状态大于0,代表被节点被取消if (s == null || s.waitStatus > 0) {s = null;//将队列中的所有节点都向前移动for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}//不为空则进行唤醒操作if (s != null)//底层调用的UnSafe类的方法 park:阻塞当前线程, unpark:使给定的线程停止阻塞LockSupport.unpark(s.thread);
}

3. 其他情况的获取同步状态

除此之外,独占模式下 AQS 还提供了两个获取同步状态的方法,可中断的获取同步状态和超时获取同步状态。

acquire 方法获取锁失败的线程是不能被 interrupt 方法中断的,所以提供了另一个方法 ,从而让获取锁失败等待的线程可以被中断。底层源码与

public final void acquireInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())//中断则抛出异常throw new InterruptedException();if (!tryAcquire(arg))doAcquireInterruptibly(arg);
}

通过调用 tryAcquireNanos 可以在超时时间内获取同步状态,可以理解为是上述中断获取同步状态的增强版。


public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())//中断则抛出异常throw new InterruptedException();return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}

上面个两个方法的源码均与普通的独占获取同步状态的源码基本类似,感兴趣的话可以自行阅读,这里不做赘述。

(五) 共享模式

资源可同时被多个线程获取,如 Semaphore、CountDownLatch。

1. 获取同步状态

代码如下,调用 acquireShared 方法可以获取同步状态,底层就是先调用须重写方法中的 tryAcquireShared。

tryAcquireShared 返回值的含义

  • 负数:表示获取资源失败
  • 0:表示获取资源成功,但是没有剩余资源
  • 正数:表示获取资源成功,还有剩余资源

public final void acquireShared(int arg) {//调用须重写方法中的tryAcquireSharedif (tryAcquireShared(arg) < 0)//获取资源失败,将当前线程放入队列的尾部并阻塞doAcquireShared(arg);
}

若获取资源失败,调用如下方法将当前线程放入队列的尾部并阻塞,直到有其他线程释放资源并唤醒当前线程。

//部分方法与独占模式下的方法公用,这里不再重复说明,详情见独占模式下的获取同步状态源码。
private void doAcquireShared(int arg) {//将当前线程封装成独占式模式的节点,添加到AQS的队列尾部,源码在独占模式中已分析。final Node node = addWaiter(Node.SHARED);//获取资源失败标识boolean failed = true;try {//线程被打断表示boolean interrupted = false;//死循环,类似 while(1)for (;;) {//获取当前节点的前趋节点final Node p = node.predecessor();//前趋节点是head,即队列的第二个节点,可以尝试获取资源if (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {//将当前节点设置为头节点,若还有剩余资源,则继续唤醒队列中后面的线程。setHeadAndPropagate(node, r);p.next = null; // help GC 表示head节点出队列if (interrupted)selfInterrupt();failed = false;return;}}//判断当前线程是否可以进入waitting状态,源码在独占模式中已分析。if (shouldParkAfterFailedAcquire(p, node) &&//阻塞当前线程,源码在独占模式中已分析。parkAndCheckInterrupt()) interrupted = true;}} finally {if (failed)//取消获取同步状态,源码见下方的取消获取同步状态章节cancelAcquire(node);}
}/** propagate就是tryAcquireShared的返回值*	● 负数:表示获取资源失败*	● 0:表示获取资源成功,但是没有剩余资源*	● 正数:表示获取资源成功,还有剩余资源*/
private void setHeadAndPropagate(Node node, int propagate) {//将当前节点设置为头节点,源码在独占模式中已分析。Node h = head; //这时的h是旧的headsetHead(node);// propagate > 0:还有剩余资源// h == null 和 h = head) == null: 不会成立,因为addWaiter已执行// waitStatus < 0:若没有剩余资源,但waitStatus又小于0,表示可能有新资源释放// 括号中的 waitStatus < 0: 这里的 h 是此时的新的head(当前节点),if (propagate > 0 || h == null || h.waitStatus < 0 ||(h = head) == null || h.waitStatus < 0) {//获取当前节点的后继节点Node s = node.next;//后继节点不存在或者是共享锁都需要唤醒,可理解为只要后继节点不是独占模式,都要唤醒//可能会导致不必要的唤醒if (s == null || s.isShared())//唤醒操作在此方法中,详情见下方的释放源码doReleaseShared();}
}

2. 释放同步状态

代码如下,调用 releaseShared 方法可以释放同步状态,底层就是先调用须重写方法中的 tryReleaseShared。

public final boolean releaseShared(int arg) {调用须重写方法中的tryReleaseSharedif (tryReleaseShared(arg)) {//尝试释放资源成功,会继续唤醒队列中后面的线程。doReleaseShared();return true;}return false;
}//唤醒队列中后面的线程
private void doReleaseShared() {//死循环,自旋操作for (;;) {//获取头节点Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;//signal表示后继节点需要被唤醒if (ws == Node.SIGNAL) {//自旋将头节点的waitStatus状态设置为0if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;            // loop to recheck cases//唤醒头节点的后继节点,源码见独占模式的释放unparkSuccessor(h);}//后继节点不需要唤醒,则把当前节点状态设置为PROPAGATE确保以后可以传递下去else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;                // loop on failed CAS}//判断头节点是否变化,没有则退出循环。//有变化说明其他线程已经获取了同步状态,需要进行重试操作。if (h == head)                   // loop if head changedbreak;}
}

(六) 取消获取同步状态

无论是独占模式还是共享模式,所有的程获取同步状态的过程中,如果发生异常或是超时唤醒等,都需要将当前的节点出队,源码如下。

//一般在获取同步状态方法的finally块中
private void cancelAcquire(Node node) {if (node == null)return;node.thread = null;		//当前线程节点设为nullNode pred = node.prev;		//获取前驱节点//前趋节点为取消状态,向前遍历找到非取消状态的节点while (pred.waitStatus > 0)node.prev = pred = pred.prev;Node predNext = pred.next;	//获取非取消节点的下一个节点node.waitStatus = Node.CANCELLED;	//将当前节点的等待状态设为取消状态//当前节点是尾节点,则自旋将尾节点设置为前一个非取消节点if (node == tail && compareAndSetTail(node, pred)) {//将尾节点设为前一个非取消的节点,并将其后继节点设为null,help GCcompareAndSetNext(pred, predNext, null);} else {int ws;//用于表示等待状态//pred != head:前一个非取消的节点非头节点也非尾节点//ws == Node.SIGNAL:当前等待状态为待唤醒//若不是待唤醒则CAS设置为待唤醒状态//前一个非取消的节点的线程不为nullif (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {//符合所有条件后,获取当前节点的后继节点Node next = node.next;if (next != null && next.waitStatus <= 0)//前一个非取消的节点的后继节点设为当前节点的后继节点//这样当前节点以及之前的已取消节点都会被移除compareAndSetNext(pred, predNext, next);} else {//前一个非取消的节点为头节点//唤醒后继节点的线程,详情见独占模式释放同步状态的源码//唤醒是为了执行shouldParkAfterFailedAcquire()方法,详解见上面的acquireQueued源码//该方法中从后往前遍历找到第一个非取消的节点并将中间的移除队列unparkSuccessor(node);}//移除当前节点node.next = node; // help GC}
}

三、总结

AQS 是用来构建锁或其他同步器的基础框架,底层是一个双端队列。支持独占和共享两种模式下的资源获取与释放,基于 AQS 可以自定义不同类型的同步组件。

在独占模式下,获取同步状态时,AQS 维护了一个双端队列,获取失败的线程都会被加入到队列中进行自旋,移出队列的条件就是前趋节点为 head 节点并成功获取同步状态。释放同步状态时,会唤醒 head 节点的后继节点。

在共享模式下,获取同步状态时,同样维护了一个双端队列,获取失败的的线程也会加入到队列中进行自旋,移除队列的条件也与独占模式一样。

但是在唤醒操作上,在资源数量足够的情况下,共享模式会将唤醒事件传递到后面的共享节点上,进行了后续节点的唤醒,解所成功后仍会唤醒后续节点。

关于 AQS 重要的几个组件的特点、原理以及对应的应用场景,后续会单独写一篇文章。若发现其他问题欢迎指正交流。


参考:

[1] 翟陆续/薛宾田. Java 并发编程之美.

[2] 方腾飞/魏鹏/程晓明. Java 并发编程的艺术.

[3] Lev Vygotsky. Java 并发编程实践

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

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

相关文章

Element中的日期时间选择器DateTimePicker和级联选择器Cascader

简述&#xff1a;在Element UI框架中&#xff0c;Cascader&#xff08;级联选择器&#xff09;和DateTimePicker&#xff08;日期时间选择器&#xff09;是两个非常实用且常用的组件&#xff0c;它们分别用于日期选择和多层级选择&#xff0c;提供了丰富的交互体验和便捷的数据…

CD4017 – 带解码输出的十进制计数器

CD4017 IC 是一个十进制计数器&#xff0c;它有 10 个输出&#xff0c;分别代表 0 到 9 的数字。计数器在&#xff08;14号引脚&#xff09;每个时钟脉冲上升时增加 1。计数器达到 9 后&#xff0c;它会在下一个时钟脉冲时从 0 重新开始。 引脚名称管脚 &#xff03;类型描述VD…

arthas命令使用

dashboard(线程、内存等环境概览) jvm&#xff08;JVM相关信息概览&#xff09; 1、RUNTIME&#xff08;系统运行环境JVM相关信息&#xff0c;运行时长等&#xff09; 2、CLASS-LOADING&#xff08;类加载信息&#xff09; 3、 COMPILATION&#xff08;编译信息&#xff09; 4…

Qt 网络编程实战

一.获取主机的网络信息 需要添加network模块 QT core gui network主要涉及的类分析 QHostInfo类 QHostInfo::localHostName() 获取本地的主机名QHostInfo::fromName(const QString &) 获取指定主机的主机信息 addresses接口 QNetworkInterface类 QNetworkInterfac…

Python——面向对象编程(类和对象)2

目录 私有属性和私有方法 01.应用场景及定义方式 02.伪私有属性和私有方法 继承 1.1继承的概念、语法和特点 1.继承的语法&#xff1a; 2.专业术语&#xff1a; 3.继承的传递性 1.2方法的重写 1.覆盖父类的方法 2.对父类方法进行扩展 关于super 1.3 父类的私有属性和…

机械拆装-基于Unity-装配功能的实现

目录 1. 装配场景的相机控制 2. 鼠标拖拽和旋转功能的实现 2.1 鼠标拖拽 2.2 物体旋转 3. 零件与装配位置的对应关系 4. 轴向装配的准备位置 5. 装配顺序的实现 5.1 标签提示 5.2 定义一个变量记录步骤数值 1. 装配场景的相机控制 开始装配功能时&#xff0c;需要将相机调…

vector与list的简单介绍

1. 标准库中的vector类的介绍&#xff1a; vector是表示大小可以变化的数组的序列容器。 就像数组一样&#xff0c;vector对其元素使用连续的存储位置&#xff0c;这意味着也可以使用指向其元素的常规指针上的偏移量来访问其元素&#xff0c;并且与数组中的元素一样高效。但与数…

1975react社区问答管理系统开发mysql数据库web结构node.js编程计算机网页源码

一、源码特点 react 社区问答管理系统是一套完善的完整信息管理类型系统&#xff0c;结合react.js框架和node.js后端完成本系统&#xff0c;对理解react node编程开发语言有帮助系统采用node框架&#xff08;前后端分离&#xff09;&#xff09;&#xff0c;系统具有完整的源…

6种ETL计算引擎介绍

目录 一、ETL计算引擎定义 二、ETL计算引擎的功能和特性 三、6种ETL计算引擎 1、MapReduce 2、Tez 3、Spark 4、Flink 5、ClickHouse 6、Doris 一、ETL计算引擎定义 ETL&#xff08;Extract, Transform, Load&#xff09;计算引擎是用于执行ETL过程中数据转换阶段的关键组件之一…

mac如何压缩视频大小不改变画质,mac怎么压缩视频软件

在数字时代&#xff0c;视频已成为信息传递和娱乐消遣的重要媒介。然而&#xff0c;视频带来的愉悦体验背后&#xff0c;是日益增长的存储和分享压力。大视频文件不仅占用大量存储空间&#xff0c;上传和下载也变得异常缓慢。那么&#xff0c;如何才能有效压缩视频&#xff0c;…

ERROR: No matching distribution found for numpy

1.原因&#xff1a; 出现这两行英文是因为原先输入pip install numpy的方式不安全&#xff0c;不被信任所以无法下载。 2.解决方法&#xff1a; 改成以下命令执行&#xff1a; pip install numpy -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun…

2025年中国国际新能源汽车技术零部件及服务展览会

中国国际新能源汽车技术零部件及服务展览会&#xff0c;从设计到制造、从使用到服务&#xff0c;精准“链”接新能源汽车全产业链的技术供应商和汽车制造商&#xff0c;专业面向新能源造车供应链的行业盛会。2024展会回顾&#xff1a;在展会的3天里&#xff0c;有62家车企核心供…

共享拼购:创新商业模式引领小用户基数下的销售奇迹“

在瞬息万变的商业蓝海中&#xff0c;一个新颖且深具潜力的策略正悄然改变着游戏规则&#xff0c;它巧妙地避开了传统路径的束缚&#xff0c;以微妙却深远的调整&#xff0c;开辟出了一条通往成功的独特航道。我的一位合作伙伴&#xff0c;正是这一策略的实践者&#xff0c;他在…

数字媒体技术基础之:DNG 文件

DNG&#xff08;Digital Negative&#xff09;文件是一种用于存储原始图像数据的文件格式&#xff0c;由 Adobe Systems 于2004年开发并推广。DNG 是一种开放的、非专利的原始图像格式&#xff0c;旨在为不同相机制造商提供一个统一的存储格式。DNG 文件保存了原始的、未处理的…

C++时区转换

#include <iostream> #include "cctz/civil_time.h" #include "cctz/time_zone.h"// 时区转换库 // https://github.com/google/cctzint test() {for (cctz::civil_day d(2016, 2, 1); d < cctz::civil_month(2016, 3); d) {std::cout << &…

【设计模式】设计模式学习线路与总结

文章目录 一. 设计原则与思想二. 设计模式与范式三. 设计模式进阶四. 项目实战 设计模式主要是为了改善代码质量&#xff0c;对代码的重用、解耦以及重构给了最佳实践&#xff0c;如下图是我们在掌握设计模式过程中需要掌握和思考的内容概览。 一. 设计原则与思想 面向对象编…

qt6 获取百度地图(一)

需求分析&#xff1a; 要获取一个地图&#xff0c; 需要ip 需要根据ip查询经纬度 根据经纬度查询地图 另外一条线是根据输入的地址 查询ip 根据查询到的ip查地图‘ 最后&#xff0c;要渲染地图 上面这这些动作&#xff0c;要进行http查询&#xff1a; 为此要有三个QNet…

机器学习与AI大数据的融合:开启智能新时代

在当今这个信息爆炸的时代&#xff0c;大数据和人工智能&#xff08;AI&#xff09;已经成为推动社会进步的强大引擎。作为AI核心技术之一的机器学习&#xff08;Machine Learning, ML&#xff09;&#xff0c;与大数据的深度融合正引领着一场前所未有的科技革命&#xff0c;不…

电子画册制作的小秘密都在这篇文章了

电子画册作为现代营销和展示的重要工具&#xff0c;已经成为各类企业和个人宣传品的首选。相比传统印刷画册&#xff0c;电子画册不仅节省成本&#xff0c;还能通过多媒体元素增强视觉冲击力&#xff0c;提升用户互动体验。本文将介绍电子画册制作的基础步骤和关键要点&#xf…

电气-伺服(4)CANopen

一、CAN Controller Area Network ,控制器局域网&#xff0c;80年的德国Bosch的一家公司研发可以测量仪器直接的实时数据交换而开发的一款串行通信协议。 CAN发展历史 二、CAN 的osi 模型 CAN特性&#xff1a; CAN 的数据帧 三、CANopen 什么是CANopen CANopen 的网络模型 …