AQS 基本思想与源码分析

充分了解 AbstractQueuedSynchronizer 对于深入理解并发编程是有益处的,它是用来构建锁或者其他同步组件的基础框架,我们常用的同步工具类如 CountDownLatch、Semaphore、ThreadPoolExecutor、ReentrantLock 和 ReentrantReadWriteLock 内部都用到了它。

以上提到的同步工具类,都是用静态内部类继承了 AQS。因此,平时在使用这些同步工具类时,我们感觉不到 AQS 的存在。

AbstractQueuedSynchronizer 中有一个很重要的变量 state:

    /*** The synchronization state.* 当 state 为 0 时,表示锁没有被占用。*/private volatile int state;

不论是 JDK 还是我们自定义的锁/工具类,都是在围绕这个 state 做文章。比如上锁时要将 state 置为 1,而解锁时将其置为 0,只不过并不是由自定义的锁直接操作,而是通过 AQS 去直接操作 state。

可以这样理解锁和 AQS 之间的关系:

  • 锁是面向使用者的,它定义了使用者与锁交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节。
  • AQS 面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

锁和 AQS 很好地隔离了使用者和实现者所需关注的领域。

1、AQS 的使用方法

1.1 模板方法

AQS 使用了模板方法设计模式,要实现自定义的同步工具类,推荐使用静态内部类继承 AQS,并根据需求重写对应的模板方法:

    /*** 尝试以独占模式获取同步状态。该方法应该查询对象的状态是否允许在独占模式下被获取,* 如果允许就去获取它。** 这个方法总是被执行获取的线程调用。如果这个方法返回 false,* 获取方法可能会将尚未排队的线程排队,直到其他线程发出释放信号。* 这个可以用来实现 {@link Lock#tryLock()} 方法。* * @param arg 获取参数。这个值总是传递给获取方法,或者是在进入条件等待时保存。*            否则,该值是未解释的,可以表示你喜欢的任何值。* @return true 表示成功。一旦成功,这个对象就被获得了。* @throws IllegalMonitorStateException 如果获取动作会将此同步器置于非法状态就抛出这个异常。*         必须以一致的方式抛出此异常,才能使同步正常工作。* @throws UnsupportedOperationException 如果不支持独占模式抛出此异常。**/protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}/*** 在独占模式下,尝试设置 state 状态变量来反映释放操作。** 这个方法总是被执行释放操作的线程调用。** @param arg 释放参数. 这个值总是传递给一个释放方法,或者是进入等待条件时的当前状态值。*            否则,该值是未解释的,可以表示您喜欢的任何值。* @return 如果这个对象当前处于完全释放的状态,就返回 true,这样在等待中的线程就可以尝试*         去获取。否则,返回 false。*       * @throws IllegalMonitorStateException,UnsupportedOperationException*         同 tryAcquire() 方法*/protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}/*** 尝试在共享模式下获取。该方法应该查询对象的状态是否允许在共享模式下被获取,* 如果允许就去获取它。** 参数、返回值、抛出的异常同 tryAcquire(),只不过本方法是在共享模式下。*/protected int tryAcquireShared(int arg) {throw new UnsupportedOperationException();}/*** 共享模式下的 tryRelease()。*/protected boolean tryReleaseShared(int arg) {throw new UnsupportedOperationException();}/*** 如果同步是被当前(调用)线程独占的,就返回 true。* 每次调用 ConditionObject 类中非等待的方法时,都会调用此方法。(等待的方法会调用 release())* 这个方法只在 ConditionObject 的方法内部调用,因此如果没用到 Condition 就不用定义这个方法。*/protected boolean isHeldExclusively() {throw new UnsupportedOperationException();}

除此之外,还有一些模板方法可能会在实现自定义锁的过程中被调用到:

这些模板方法负责独占式&共享式获取与释放同步状态,同步状态和查询同步队列中的等待线程情况。

此外,跟 state 相关的还有三个方法 getState()、setState()、compareAndSetState() 分别用来获取同步状态、设置同步状态、使用 CAS 设置同步状态。

1.2 举例

如果要实现一个独占锁,就要重写 tryAcquire()、tryRelease() 和 isHeldExclusively(),而实现共享锁就要重写 tryAcquireShared()、tryReleaseShared() 。

比如我要自定义一个不可重入的显式独占锁,那么就要实现 Lock 接口,定义静态内部类继承 AQS,实现获取锁、释放锁等方法:

public class MyLock implements Lock {/* MyLock 有关锁的操作仅需要将操作代理到 Sync 上即可*/private final Sync sync = new Sync();private final static class Sync extends AbstractQueuedSynchronizer {// 判断处于独占状态@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}// 获得锁@Overrideprotected boolean tryAcquire(int i) {// CAS 操作设置 state 保证同步if (compareAndSetState(0, 1)) {// 设置占有独占锁的线程setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}// 释放锁@Overrideprotected boolean tryRelease(int i) {if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}// 返回一个Condition,每个condition都包含了一个condition队列public Condition newCondition() {return new ConditionObject();}}@Overridepublic void lock() {System.out.println(Thread.currentThread().getName() + " ready get lock");sync.acquire(1);System.out.println(Thread.currentThread().getName() + " already got lock");}@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}@Overridepublic boolean tryLock(long timeout, TimeUnit timeUnit) throws InterruptedException {return sync.tryAcquireNanos(1, timeUnit.toNanos(timeout));}@Overridepublic void unlock() {System.out.println(Thread.currentThread().getName() + " ready release lock");sync.release(1);System.out.println(Thread.currentThread().getName() + " already released lock");}@Overridepublic Condition newCondition() {return sync.newCondition();}
}

Lock 接口中的方法实现都是借助于静态内部类 Sync 的实例,调用相应的方法即可。在 lock() 和 unlock() 中分别调用 Sync 对象的 acquire() 和 release() 方法,其内部调用了 Sync 内部重写的 tryAcquire() 和 tryRelease():

AbstractQueuedSynchronizer.java:public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

2、AQS 的基本思想 CLH 队列锁

AQS 以 CLH(Craig、Landin、Hagersten 三人名字首字母)队列锁为基本思想,而 CLH 队列锁是一种基于链表的可扩展、高性能、公平的 FIFO 自旋锁。申请锁的线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。

先把申请锁的线程打包成一个 QNode,myPred 指向前驱节点,locked 表示当前线程是否需要锁:

QNode

QNode 入队要按照先后顺序排列,当线程 A 需要获取锁时,要把包装该线程的 QNode 节点加入到 CLH 队列尾部,并且把 locked 设为 true:

线程A需要获取锁时

随后线程 B 也需要获取锁被加入到 CLH 队列尾部:

线程B需要获取锁时

加入队列的 QNode 节点要自旋检测前一个节点的 locked 是否变为了 false,变成 false 就认为前一个节点已经释放了锁,该轮到当前节点获取锁了:

线程A获得了锁

QNode_A 拿到锁后停止自旋。在线程执行完任务释放锁后,也要把 locked 置为 false。

AQS 在实现时是基于 CLH 算法但是又做了很多改进,例如在 QNode 中加入 next 域实现双向队列,自旋检测锁状态时有次数限制等。

3、AQS 锁流程源码分析

AQS 使用头指针(head)与尾指针(tail)参与管理这个双向同步队列。其一般工作流程为:

  1. 初始状态下,head 与 tail 都为 null。当有线程获取同步器失败后,需要打包成 Node 节点进入同步队列,此时先通过 new Node() 创建一个空节点作为头节点,并且让尾节点也指向这个初始头节点。然后把入队节点添加到尾节点之后,由于可能会有多个竞争失败的线程所在的节点都要做入队操作,因此添加到队尾的这个动作要使用 CAS 原子操作,CAS 失败的 Node 要自旋直到成功添加到队尾为止。(addWaiter() 和 acquireQueued() 方法)
  2. 在同步队列中等待获取同步器的节点,也会进行自旋操作,尝试再次获取同步器。规则是只有当前节点的前驱节点是头节点才有获取同步器的资格,如果该节点成功获取到,那么就释放掉原来的头节点,并把当前节点设置为新的头节点(也就是说头节点一般是占有锁且正在执行线程任务的节点)。
  3. 队列中的线程如果一直无法获得同步器,那么在一定时间后就要先中断,一定时间是通过前驱节点的等待状态变化决定的。所有入队节点的等待状态 waitStatus 初始值都为 0,在上一步的自旋过程中,如果当前节点没有获取到同步器,那么就要给前驱节点的 waitStatus 设置为 SIGNAL 表示将要把后继节点(前驱结点的后继节点就是当前节点)阻塞。待自旋进入第二次循环时,当前节点如果还没拿到同步器,并且检查到它的前置节点已经是 SIGNAL 状态了,就要使用 LockSupport.park() 暂停当前线程,直到它的前置节点执行完任务释放同步器时再唤醒它。

3.1 acquire()

在自定义锁时,需要调用 AQS 的 acquire() 来获取锁:

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 其实就是调用 Thread.currentThread().interrupt()selfInterrupt();}

如果 tryAcquire() 返回 true 说明拿到了锁,拿锁过程直接结束,后续代码就不会执行。否则,要先后执行 addWaiter() 和 acquireQueued()。

3.2 addWaiter()

addWaiter() 会在线程获取锁失败后,将该线程封装进 Node 并将其入队:

    /*** 等待队列的队尾,延迟初始化。只有在队尾添加新的等待 Node 时* 才由 enq 方法修改。*/private transient volatile Node tail;/*** Creates and enqueues node for current thread and given mode.** @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared* @return the new node*/private Node addWaiter(Node mode) {// 把当前线程包装成一个 NodeNode node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {// 如果尾节点不为空,就让 node 的 prev 字段指向这个尾节点 pred。node.prev = pred;// CAS 操作把 node 设置为队列尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 如果队列没有初始化,则执行初始化操作,否则 CAS 把 node 加入到队列。enq(node);return node;}/*** Inserts node into queue, initializing if necessary. See picture above.* @param node the node to insert* @return node's predecessor*/private Node enq(final Node node) {// 自旋,直到成功把 node 添加为队列尾节点。for (;;) {Node t = tail;if (t == null) { // Must initialize// 原子操作设置头节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;// 原子操作设置尾节点if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

addWaiter() 内部在添加一个 Node 到队尾时,会先直接用 compareAndSetTail() 尝试将 Node 加到队尾。如果尝试失败,才会在 enq() 内自旋,直到成功的将 Node 加到队尾。

3.3 acquireQueued()

acquireQueued() 负责在独占且不可中断模式下,为已经在队列中的线程获取同步器。该方法会返回一个布尔类型值 interrupted 表示当前线程是否应该被中断:

    /*** Acquires in exclusive uninterruptible mode for thread already in* queue. Used by condition wait methods as well as acquire.** @param node the node* @param arg the acquire argument* @return {@code true} if interrupted while waiting*/final boolean acquireQueued(final Node node, int arg) {// 因为 acquire() 的 if 语句中 tryAcquire() 失败了,因此这里的 failed 初始化为 true。boolean failed = true;try {boolean interrupted = false;// 自旋,再次用 tryAcquire() 尝试拿锁for (;;) {final Node p = node.predecessor();// 如果当前节点的前驱结点是头节点,并且当前节点拿锁成功if (p == head && tryAcquire(arg)) {// 把当前节点设为头节点,并把原来的头节点从队列中删除并返回 interrupted。setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 检查前驱节点状态,并执行中断操作if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {// 如果失败,就取消当前节点获取锁的操作。if (failed)cancelAcquire(node);}}

shouldParkAfterFailedAcquire() 会根据前驱节点的 waitStatus 状态判断是否应该在当前节点获取锁失败之后中断当前线程,返回 true 表示应该中断:

    /*** Checks and updates status for a node that failed to acquire.* Returns true if thread should block. This is the main signal* control in all acquire loops.  Requires that pred == node.prev.** @param pred node's predecessor holding status* @param node the node* @return {@code true} if thread should block*/private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 前驱节点状态如果已经是 SIGNAL,就应该中断当前线程int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;// 前驱节点状态值大于 0 说明其取消获取线程,从队列中移除这些节点if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

pred 是 node 的前驱结点,根据前驱结点的状态做不同的操作:

  • 如果是 Node.SIGNAL,那么就返回 true 表示前驱节点的后继节点,也就是当前节点需要被 park;
  • 如果状态数值大于 0,说明前驱节点处于取消状态,那么就要向前找,一直找到一个不是取消状态的节点,让这个节点作为 node 的前驱节点;
  • 如果是 0 或者 PROPAGATE(-3),就用 CAS 操作把这个前驱节点的状态值设置为 Node.SIGNAL。

这里注意,每个节点的 waitStatus 初始值都为 0,在 acquireQueued() 的 for 循环中,第一次循环假设仍没有拿到锁,那么进入到 shouldParkAfterFailedAcquire(),把前驱结点的 waitStatus 置为 Node.SIGNAL 并返回 false;然后执行第二次 for 循环,假设也没拿到锁,那么 shouldParkAfterFailedAcquire() 就会返回 true,使得 parkAndCheckInterrupt() 得以执行:

    /*** Convenience method to park and then check if interrupted** @return {@code true} if interrupted*/private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

其实就是使用 LockSupport.park() 暂停当前线程,并返回线程状态。如果线程已经中断,那么设置 interrupted 为 true。

3.4 release()

释放锁的操作就很简单了:

    public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}/*** Wakes up node's successor, if one exists.** @param node the node*/private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/// 传入的 node 是头节点,要找到头节点后第一个不是取消状态的节点,唤醒其中的线程。Node s = node.next;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)LockSupport.unpark(s.thread);}

tryRelease() 为 true 表示成功释放了同步器,那么就要用 LockSupport.unpark() 把当前节点(即头节点)后第一个不是 CANCELLED 状态的节点(如果有的话)中的线程唤醒。

4、总结

AQS 是公平锁(因为新来的节点都是加到队尾)。公平锁与非公平锁的实现几乎一模一样,在 ReentrantLock 中,提供了 FairSync 和 NonfairSync 两个内部类,分别实现公平锁和非公平锁。比较这两个内部类的代码,几乎一模一样,只不过在 tryAcquire() 拿锁时,公平锁要先判断队列中是否有前驱节点已经在等待中(hasQueuedPredecessors())。

不可重入锁在同一线程做递归调用时会发生死锁,需要在自定义锁时对 state 做特殊处理(即实现可重入锁,一般都是要实现可重入锁的)。

AQS 参考资料:
AbstractQueuedSynchronizer的介绍和原理分析
深入理解AbstractQueuedSynchronizer(AQS)
[官方文档AbstractQueuedSynchronizer](

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

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

相关文章

理解位图算法:使用 C++ 实现高效数据查重

在处理海量数据时&#xff0c;我们常常需要检查某个元素是否已经存在于集合中。传统的方法如哈希表或集合容器虽然有效&#xff0c;但在数据量极大的情况下会占用大量内存。这时&#xff0c;位图算法 (Bitmap) 就成为了一种非常高效的解决方案。本文将通过分析一段使用位图算法…

数学复习笔记 12

前言 现在做一下例题和练习题。矩阵的秩和线性相关。另外还要复盘前面高数的部分的内容。奥&#xff0c;之前矩阵的例题和练习题&#xff0c;也没有做完&#xff0c;行列式的例题和练习题也没有做完。累加起来了。以后还是得学一个知识点就做一个部分的内容&#xff0c;日拱一…

1-10 目录树

在ZIP归档文件中&#xff0c;保留着所有压缩文件和目录的相对路径和名称。当使用WinZIP等GUI软件打开ZIP归档文件时&#xff0c;可以从这些信息中重建目录的树状结构。请编写程序实现目录的树状结构的重建工作。 输入格式: 输入首先给出正整数N&#xff08;≤104&#xff09;…

Python爬虫实战:研究 RPC 远程调用机制,实现逆向解密

1. 引言 在网络爬虫技术的实际应用中,目标网站通常采用各种加密手段保护其数据传输和业务逻辑。这些加密机制给爬虫开发带来了巨大挑战,传统的爬虫技术往往难以应对复杂的加密算法。逆向解密作为一种应对策略,旨在通过分析和破解目标网站的加密机制,获取原始数据。 然而,…

debugfs:Linux 内核调试的利器

目录 一、什么是 debugfs&#xff1f;二、debugfs 的配置和启用方式2.1 内核配置选项2.2 挂载 debugfs2.3 Android 系统中的 debugfs 三、debugfs 的典型应用场景3.1 调试驱动开发3.2 内核子系统调试3.3 性能分析 四、常见 debugfs 子目录与功能示例4.1 /sys/kernel/debug/trac…

lua 作为嵌入式设备的配置语言

从lua的脚本中获取数据 lua中栈的索引 3 | -1 2 | -2 1 | -3 可以在lua的解释器中加入自己自定的一些功能,其实没啥必要,就是为了可以练习下lua

棋牌室台球室快速接入美团团购接口

北极星平台从2024年12月份开始慢慢关闭&#xff0c;现在很多开发者反馈北极星token已经不能刷新了&#xff0c;全部迁移到美团团购综合平台。 申请这个平台要求很高 1、保证金费用要15万起步 2、平台必须是二级等保和安全产品 &#xff0c;一个二级等保费用10万起步 所以很多…

开源轻量级地图解决方案leaflet

Leaflet 地图&#xff1a;开源轻量级地图解决方案 Leaflet 是一个开源的 JavaScript 库&#xff0c;用于在网页中嵌入交互式地图。它以轻量级、灵活性和易用性著称&#xff0c;适用于需要快速集成地图功能的项目。以下是关于 Leaflet 的详细介绍和使用指南。 1. Leaflet 的核心…

一个批量文件Dos2Unix程序(Microsoft Store,开源)1.1.0 编码检测和预览

之前的版本是个意思意思&#xff0c;验证商店发布的&#xff08;其实是我以前自己用的工具&#xff09;&#xff0c;这次把格式检查和转换都做上了&#xff0c;功能应该差不多了&#xff0c;还有一些需要小改进的地方。 因为还没什么用户嘛&#xff0c;还是保持全功能免费试用。…

特征提取:如何从不同模态中获取有效信息?

在多模态学习中&#xff0c;不同模态&#xff08;文本、图像、语音、视频、传感器数据等&#xff09;所携带的信息丰富且互补。但不同模态的数据结构、表示空间、时空分布截然不同&#xff0c;因此&#xff0c;如何对各模态进行高效、有效的特征提取&#xff0c;是整个多模态学…

Go语言爬虫系列教程 实战项目JS逆向实现CSDN文章导出教程

爬虫实战&#xff1a;JS逆向实现CSDN文章导出教程 在这篇教程中&#xff0c;我将带领大家实现一个实用的爬虫项目&#xff1a;导出你在CSDN上发布的所有文章。通过分析CSDN的API请求签名机制&#xff0c;我们将绕过平台限制&#xff0c;获取自己的所有文章内容&#xff0c;并以…

交叉熵损失函数,KL散度, Focal loss

交叉熵损失函数&#xff08;Cross-Entropy Loss&#xff09; 交叉熵损失函数&#xff0c;涉及两个概念&#xff0c;一个是损失函数&#xff0c;一个是交叉熵。 首先&#xff0c;对于损失函数。在机器学习中&#xff0c;损失函数就是用来衡量我们模型的预测结果与真实结果之间…

149.WEB渗透测试-MySQL基础(四)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;148.WEB渗透测试-MySQL基础&#xff08;三&#xff09; 非关系型数据库&#xff1a; &a…

c/c++中程序内存区域的划分

c/c程序内存分配的几个区域&#xff1a; 1.栈区&#xff1a;在执行函数时&#xff0c;函数内局部变量的存储单元都可以在栈上创建&#xff0c;函数执行结束时这些存储单元自动被释放&#xff0c;栈内存分配运算内置于处理器的指令集中&#xff0c;效率很高但是分配的内存容量有…

构建稳定的金字塔模式生态:从自然法则到系统工程

在自然界中&#xff0c;金字塔结构广泛存在于生态系统之中&#xff0c;表现为营养级能量金字塔、生物量金字塔和数量金字塔等形式。这种结构不仅形象地描述了生态能量流转的规律&#xff0c;也体现出生态系统中“稳定性”与“层级性”的天然法则。在现代软件架构、企业组织、平…

Vue 3.0双向数据绑定实现原理

Vue3 的数据双向绑定是通过响应式系统来实现的。相比于 Vue2&#xff0c;Vue3 在响应式系统上做了很多改进&#xff0c;主要使用了 Proxy 对象来替代原来的 Object.defineProperty。本文将介绍 Vue3 数据双向绑定的主要特点和实现方式。 1. 响应式系统 1.1. Proxy对象 Vue3 …

TIP-2021《SRGAT: Single Image Super-Resolution With Graph Attention Network》

推荐深蓝学院的《深度神经网络加速&#xff1a;cuDNN 与 TensorRT》&#xff0c;课程面向就业&#xff0c;细致讲解CUDA运算的理论支撑与实践&#xff0c;学完可以系统化掌握CUDA基础编程知识以及TensorRT实战&#xff0c;并且能够利用GPU开发高性能、高并发的软件系统&#xf…

大语言模型与多模态模型比较

一、核心差异&#xff1a;输入数据类型与模态融合 输入数据类型 LLM&#xff1a;仅处理文本数据&#xff0c;例如文本分类、机器翻译、问答等任务&#xff0c;通过大规模语料库学习语言规律。 LMM&#xff1a;支持文本、图像、音频、视频等多种模态输入&#xff0c;例如根据图…

Apache HttpClient 5 用法-Java调用http服务

Apache HttpClient 5 核心用法详解 Apache HttpClient 5 是 Apache 基金会推出的新一代 HTTP 客户端库&#xff0c;相比 4.x 版本在性能、模块化和易用性上有显著提升。以下是其核心用法及最佳实践&#xff1a; 一、添加依赖 Maven 项目&#xff1a; <dependency><…

基于 Spark 的流量统计

一、引言 在互联网行业&#xff0c;流量统计是分析网站或应用用户行为、评估业务表现、优化资源分配以及制定营销策略的关键环节。借助 Apache Spark 强大的分布式数据处理能力&#xff0c;我们可以高效地对大规模的流量数据进行统计分析&#xff0c;获取有价值的洞察。本文将…