网上的 AQS 文章让我很失望
一、AQS 很多人都没有讲明白
🤔 翻看了网上的 AQS(AbstractQueuedSynchronizer)文章,质量参差不齐,大多数都是在关键处跳过、含糊其词,美其名曰 “传播知识” 。
大多数都是进行大段的源码粘贴和注释,或者叫源码翻译!有必要写上一篇文章,将 AQS 的一些基础原理搞清楚,搞正确。

本文尽量用图文的形式阐释过程,同时结合断点调试,将 AQS 在多线程运行下的状态,尽可能呈现出来!
二、准备和前提
注意:本文所指的 AQS 均为 Java 并发包中的 AbstractQueuedSynchronizer 类。
2.1 环境说明
- InterlliJ IDEA 2024.2 (免费使用 30 d)
- JDK1.8
2.2 线程知识储备
知识点一、 LockSupport:
深入 AQS 的源码,需要提前理解 LockSupport 接口:
LockSupport.park() : 当前线程会进入阻塞状态,直到它被 unpark 唤醒或者线程被中断
LockSupport.unpark(Thread thread): 唤醒其他线程,参数是 Thread
LockSupport 功能简单强大,对线程的挂起和唤醒非常方便。
知识点二、 ReentrantLock:
- ReentrantLock 依赖 Sync
- Sync 继承 AQS
- FaireSync 是公平锁; NonFaireSync 是非公平锁, 也是 RentrantLock 默认的锁类型

模板代码如下:
// 获取锁
lock.lock();
try {// 代码块
} finally {// 释放锁lock.unlock();
}
知识点三、CAS
- 常用的原子操作,用于在多线程编程中实现无锁的线程安全操作
- CAS 操作包含三个主要的参数:内存位置(V)、预期原值(A)和新值(B)
- 只有当内存位置的值与预期原值相匹配时,CAS 操作才会将该位置值更新为新值,并且这种检查和替换是作为一个不可分割的原子操作完成的。
知识点四:线程模式
| 模式 | 含义 |
| SHARED | 线程以共享的模式等待锁 |
| EXCLUSIVE | 线程正在以独占的方式等待锁 |
2.3 代码准备
需要断点调试,本文准备了一段代码,可以按照文章步骤逐一调试。
代码内容:三个线程,分别为 ABC,进行锁资源的抢夺。本文使用 ReentrantLock 中的非公平锁实现。

代码如下:
- ABC 三个线程,共同争夺一把锁
- 获得锁后执行 count++
- 完成后释放锁
为了有更好的阅读体验,建议先搞明白下面代码☺️。
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {// ABC 三个线程抢夺一把锁。显示指明使用非公平锁private static final ReentrantLock lock = new ReentrantLock(false);// 获取锁后对 count 进行++ 操作private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {// 线程 A Thread a = new Thread(() -> {for (int i = 0; i < 10; i++) {// 获取锁lock.lock();try {count++;System.out.println(Thread.currentThread().getName() + " incremented count to " + count);} finally {// 释放锁lock.unlock();}}}, "A");// 线程 B Thread b = new Thread(() -> {for (int i = 0; i < 10; i++) {// 抢占锁lock.lock();try {count++;System.out.println(Thread.currentThread().getName() + " incremented count to " + count);} finally {lock.unlock();}}}, "B");// 线程 B Thread c = new Thread(() -> {for (int i = 0; i < 10; i++) {// 抢占锁lock.lock();try {count++;System.out.println(Thread.currentThread().getName() + " incremented count to " + count);} finally {lock.unlock();}}}, "C");a.start();// 先让 B 线程晚一点执行System.out.println("---------");Thread.sleep(20000);b.start();// C 线程最后执行System.out.println("---------");Thread.sleep(20000);c.start();a.join();b.join();c.join();}
}
可以拷贝到自己的 IDEA 中进行调试
2.4 如何进行多线程的 debug
很多同学没有多线程的调试经验,当然多线程的调试是有难度。 希望通过本文,能够有一些帮助。

本文中的多线程调试需要掌握两个关键要的:
要点一:查看运行栈帧 && 切换线程
在 Threads & Variables 这个窗口,保障线程之间切换。

要点二:断点暂停方式,选择 Thread

这个是最为重要的。
建议本次调试:选择 Make Default,点击图中 Make Default,后续所有断点都是 Thread,如果不选择 Thread,则无法进行断点追踪!
接下来通过断点追踪的,演示 AQS 内部执行过程和原理。
将整个过程分为两个大阶段:抢锁过程和释放锁过程
- ABC 三个线程抢锁过程,分别是 A 先抢到锁,然后 B、 C 再进入抢锁
- A 获得锁执行代码后,再释放锁, 然后将 B 线程唤醒; 最后再唤醒 C
三、抢占锁过程演示
3.1 线程 A 获取锁演示

场景一: 模拟先让 A 线程获取到锁
注意在 B、C 开始的位置设置断点,这样能够控制 B、C 线程的启动时间。


先让断点执行到线程 A,并停留在线程 lock.lock() 位置。接下来进入此方法

注意:只有切换到线程 A , 才能进入到线程 A 的 lock 的源码。
点击进入源码,注意断点位置, 可以参考下图:
一些非关键的代码理解会直接跳过!比如 sync.lock();

关键代码如下:
final void lock() {// CAS 方式的设置 stateif (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
通过 CAS 将 state 设置为 1,设置成功这个线程则获得锁成功。
深入看一下 compareAndSetState 代码, 是通过 unsafe 的 compareAndSwapInt 实现的。
unsafe 类是一个功能较为底层。但并不复杂,使用上可以模仿!
protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
stateOffset 是对象 state 字段的偏移值。
stateOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
总结一下: 使用 unsafe.compareAndSwapInt(this, stateOffset, expect, update) 对 state 字段进行值的更新,如果成功则获取锁成功,否则进入其他分支。
BC 线程还未开始,只有 A 线程处于 RUNNING 状态,即 1 个线程,因此 state 可以给更新成 1。
通过断点运行,compareAndSwapInt 返回 true, 接下来再调用 setExclusiveOwnerThread(), 完成线程 A 独占访问。

设置线程独占

运行到现在,AQS 中的情况如下图所示:
A 获得独占锁,BC 未开始:

因为只有一个线程A,所以没有争抢,不需要创建队列。
接下来暂停线程 A 的调试,模拟线程B 抢锁过程。
3.2 线程 B 抢占锁演示
执行过程:
先切换到 main 线程,启动线程 B; 当线程 B 启动后,切换到线程 B, 断点调试执行线程 B 的抢占过程

compareAndSetState(0, 1) 返回为 false,执行 acquire(1)代码
步骤一:切换线程 main 线程,让线程 B 运行。

步骤二:当线程 B 运行成功后,切换线程 B,进行断点追踪。

线程 B 进入了 acqurie(1) 分支

分析 acquire 代码逻辑:

流程:tryAcquire 尝试再一次获取锁,如果失败,进入队列
进入 tryAcquire 源码进行查看:
final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 当为 0 的时候,表示没有线程在使用锁,尝试 CAS 抢锁if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 是否当前线程,实现线程的重入 else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
如果 state == 0,表示还没有独占的线程。于是尝试将当前线程设置成独占,这段代码与刚刚分析的逻辑是一样的!(做一次尝试)
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
有两个点可以提出来关注一下:
- 能够设置 state 值成功的线程即抢占锁成功,后续不用进入队列等待
- 如果获得锁的线程执行完毕,即 state = 0; 于是同时,新来一个线程,这个线程将尝试获得锁,而不是将其加入到队尾。这一点体现了不公平锁的特性!不是按照FIFO
特别说明:如果 state = 0 并不表示队列中没有正在等待的线程。
如果当前线程与获得独占锁的线程是同一个线程,允许 state 修改。
else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;
}
因为是相同的线程,所以这段代码也是线程安全的。
注意:这也是 ReentrantLock 能够实现重入的关键代码。 如下面,进行两次 lock。 那么 state 会变成 2。
public void performAction() {lock.lock(); // 第一次获取锁try {System.out.println("First lock acquired.");performAnotherAction(); // 调用另一个需要锁的方法} finally {lock.unlock(); // 释放锁System.out.println("First lock released.");}
}public void performAnotherAction() {lock.lock(); // 第二次获取同一个锁try {System.out.println("Second lock acquired on the same thread.");// 执行一些需要锁的操作} finally {lock.unlock(); // 释放锁System.out.println("Second lock released.");}
}

当线程不能获取到锁的时候,则进入 addWaiter 环节:
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- 先调用 addWaiter()
- 再调用 acquireQueued
3.2.1 添加等待节点
addWaiter() 和 acquireQueued() 是实现线程等待的关键
现在继续分析线程 B 的执行过程:

通过代码分析, pred 为 null,线程 B 进入 enq(node)
注意:现在的 Node 为线程 B

3.2.2 enq() 创建队列
进入 enq 方法的线程,依然存在多个线程竞争的关系。
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
通过 for(;;) 保证所有进来的线程最终都能够被合理的添加到相应的节点上。
分析线程 B 在这段代码中的过程:
- 线程 B 第一次进入 ,
t == null为 ture,将创建了一个没有任何线程绑定的节点 Node (暂且称为空信息节点) - 第二次,设置线程B 节点为尾部节点,并将头节点的 next 设置为 线程 B 节点,
上面是理想情况,进入 enq 依然是存在多线程的,所以需要通过 CAS 保证线程的运行安全。
第一次执行:enq()

第一次执行,完成头部节点地创建。当前 AQS 的状况:

完成后,将 head、tail 地址引用的方式指向刚刚 New Node() 的节点。head、tail 都是地址引用
注意:这个头节点没有任何线程信息(这一点在其他 blog 中说法错误的,要警惕!)
由于使用的是for(;;) 逻辑; 在进入第二次的时候,线程 B 这个节点将会插到队尾。

hashCode=699 :线程 B 节点
hashCode=702 :空信息头节点
在循环完成第二次后,完成对线程 B 节点的插入。如下所示:

- tail 设置成线程 B 节点
- 线程 B 节点的的 prev 指向了 head(即刚刚创建出来的空信息节点)
完成了队列的创建后,看一下接下来线程 B 到底是立刻等待,还是需要执行一些特殊逻辑后再进入等待。
3.2.3 acquireQueued
注意关键代码:for(;;)

if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;
- 先执行
shouldParkAfterFailedAcquire() - 如果
shouldParkAfterFailedAcquire()返回为 false,则parkAndCheckInterrupt()不会再执行
shouldParkAfterFailedAcquire 判断是否需要将线程暂停。注意传入参数:

补充知识点:
| 枚举 | 含义 |
| 0 | Node 初始化的时候的默认值 |
| CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
| CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
| PROPAGATE | 为-3,当前线程处在 SHARED 情况下,该字段才会使用 |
| SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
具体逻辑情况如下:

特别注意:waitStatus > 0 只有取消状态这种状态
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
第一次执行 acquireQueued ,shouldParkAfterFailedAcquire() 返回 false。
AQS 中的状态信息:完成第一次运行后,线程 B 节点的前驱节点的 waitStatus = -1 。 如下图所示:

第二次执行 acquireQueued, shouldParkAfterFailedAcquire() 返回 true。
原因是因为前驱节点的 waitStatus = -1

第一次:shouldParkAfterFailedAcquire(p, node) 执行 false,方法 parkAndCheckInterrupt()将跳过
第二次:shouldParkAfterFailedAcquire(p, node) 执行 true ,则执行 parkAndCheckInterrupt(),对线程 B 进行暂停处理。
很显然:在线程进入阻塞等待之前,线程节点多做了一次循环,算是一种优化

由于线程 B 一直获取不到锁,执行 park ,对线程阻塞挂起!
完成线程 B 的抢占演示后,再演示线程 C
3.3 线程 C 抢占锁演示
线程 C 的逻辑和线程 B 的逻辑是相似的:

A 正在获取锁,线程 C 只能执行 acquire() 分支代码
线程 C 和线程 B 一些差异点:
3.3.1 addWaiter 方法
由于 tail 已经存在,则直接将节点添加到队尾

执行完成后, AQS 的情况如下:

注意:head、tail 只是地址引用。分别指向队列的首尾。而 head 并不是一个实际的线程节点,没有线程相关信息, 这个要特别注意!

通过栈帧情况,线程 A 处于运行状态,线程 B、线程 C 都处于挂起等待状态。
3.4 抢占过程总结
- 设置 state > 0 成功的线程,AQS 的
exclusiveOwnerThread值将被设置成该线程。即这个线程获得锁 - ReentrantLock 可以重入,通过 state 来控制重入次数
- ReentrantLock 的非公平锁的原理:当新的线程进入,调用 tryAcquire()多次尝试对 state 修改,即尝试获得独占,这个时候不管是否存在阻塞线程;如果多次尝试没有获取独占机会,会将这个线程加入双向队列
- 注意:双向队列的头节点是一个不带线程信息节点
- 在线程进入阻塞状态之前,依然会判断是否能够获取到锁。如果再次失败,最终会线程进行阻塞挂起
线程 ABC 的锁抢占过程就演示完成,那么阻塞挂起的线程又如何被唤醒呢?
四、锁释放过程

4.1 释放逻辑
接下来,执行线程 A 中的释放逻辑:

调用情况如下:
java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() {sync.release(1);
}
代码逻辑:
- 释放锁,exclusiveOwnerThread 设置为 null
- state 释放对应数值
- 唤醒等待的线程
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)// 唤醒等待的线程unparkSuccessor(h);return true;}return false;
}

4.2 唤醒等待线程

关键方法: unparkSuccessor()

这个时候 AQS 的状态情况:
- state = 0
- exclusiveOwnerThread = null

线程 B 被唤醒

关键方法如下:
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);// 注意:LockSupport.park 会响应中断// 非中断返回 falsereturn Thread.interrupted();}

interrupted()是一个静态方法,它检查当前线程是否被中断,并清除中断状态。这意味着如果线程被中断,第一次调用interrupted()会返回true,并且重置中断状态。因此,如果紧接着再次调用interrupted(),它将返回false,因为中断状态已经被清除了
4.3 线程 B 获得锁
线程 B 继续执行逻辑

线程 B 获得锁执行代码,线程 C 再获得锁执行代码。由于线程 C 的释放过程和线程 B 是一样的,就不必备再赘述。
当线程 ABC 都释放后,这个时候 AQS 的状态:

最后 AQS 的状态如下:

注意 head、tail 并没有销毁,将常驻内存!
4.4 释放过程总结
- Lock.park 挂起线程
- Lock.unpark 唤醒线程
- 当线程执行完成后,会唤醒最靠前的那个线程节点,注意不是 head 节点。 head 节点是不具备线程信息的
- 整个释放过程,类似链表的删除!
4.5 公平锁 faireSync
公平锁逻辑稍微简单一些:公平锁的逻辑则完全按照队列的思想来,FIFO
思路:判断队列是否存在,如果不存在则创建,如果存在则加入到队尾
五、总结
到这里,对于 AQS 的队列、线程阻塞等问题基本算是清楚了。如果不清楚,可以按照代码一步步地进行调试,感受一下 AQS 的魅力所在。
5.1 锁抢占过程和释放过程
- 第一个线程,通过 CAS 设置 state 和 exclusiveOwnerThread 获得锁
- 第二个线程,创建一个双线队列,同时第一个节点为空信息节点 head,不携带线程相关信息;同时将该线程封装成 Node,插入到队尾
- 第三个线程,会继续插入到队尾
- 进入队列的线程会通过 LockSupport.park 进入挂起阻塞状态
- 当获得锁的线程执行完,调用释放锁的过程,会通过 LockSupport.unpark 将第一个线程节点唤醒。注意不是 head 节点。 其他线程唤醒过程类似。
5.2 AQS代码设计中的优点
- 设计模式:AQS 中的模版方法,通过 tryAcquire、tryRelease 等方法的重写;从而实现了不同的工具类。很显然结构设计是很棒的
- CAS,使用大量的 CAS 空控制线程安全
- 通过 for(;;) 等细节,多次尝试对锁的获取,避免直接将线程挂起阻塞,细节上很讲究
- 通过精心的变量值设计,诸如 state、waitStatus 等,代码更少,逻辑更清晰
5.3 未涉及知识和弊端
部分细节未深入了解,主要有以下:
- 中断获取 acquireInterruptibly
- 条件判断 condition
- 异常情况
- 以 ReentrantLock 进行讲解,有一定局限
如果有时间再挖一挖。
5.4 为什么队列中的 head 不携带线程信息
问题:队列中的 head 节点不绑定线程,为什么需要这样的 head 节点存在?
答案:一些自己的看法(不一定对和全,欢迎补充):
由于线程的生命周期非常短;如果 head 是线程节点,那么随着锁的争夺和释放,整个队列将被反复创建和销毁。
但是给一个无关的 head 信息,只创建一次,并能反复使用,常驻内存。 从整体而言,确实可以带来性能的提升!
工作这么多年,第一次对 AQS 有这么深入的理解,不多废话了,本文到此结束!
已经同步发布到微信公众号:面汤放盐:网上的 AQS 文章让我很失望

相关文章:
网上的 AQS 文章让我很失望
一、AQS 很多人都没有讲明白 🤔 翻看了网上的 AQS(AbstractQueuedSynchronizer)文章,质量参差不齐,大多数都是在关键处跳过、含糊其词,美其名曰 “传播知识” 。 大多数都是进行大段的源码粘贴和注释&…...
滑动窗口子串
文章目录 滑动窗口一、无重复字符的最长子串二、找到字符串中所有字母异位词 子串三、和为 K 的子数组四、滑动窗口最大值五、最小覆盖子串 滑动窗口 一、无重复字符的最长子串 题目链接 (方法一:暴力枚举) (方法二ÿ…...
【windows11 提示“Microsoft Visual C++ Runtime Library Runtime Error】
windows11 提示“Microsoft Visual C++ Runtime Library Runtime Error” 问题描述解决方法郑重声明:本人原创博文,都是实战,均经过实际项目验证出货的 转载请标明出处:攻城狮2015 Platform: windows OS:windows11 问题描述 解决方法 下载VisualCppRedist_AIO_x86_x64.exe 安…...
【leetcode|哈希表、动态规划】最长连续序列、最大子数组和
目录 最长连续序列 解法一:暴力枚举 复杂度 解法二:优化解法一省去二层循环中不必要的遍历 复杂度 最大子数组和 解法一:暴力枚举 复杂度 解法二:贪心 复杂度 解法三:动态规划 复杂度 最长连续序列 输入输…...
【人工智能】掌握深度学习中的时间序列预测:深入解析RNN与LSTM的工作原理与应用
深度学习中的循环神经网络(RNN)和长短时记忆网络(LSTM)在处理时间序列数据方面具有重要作用。它们能够通过记忆前序信息,捕捉序列数据中的长期依赖性,广泛应用于金融市场预测、自然语言处理、语音识别等领域…...
今日开放!24下软考机考「模拟练习平台」操作指南来啦!
2024年下半年软考机考模拟练习平台今日开放,考生可以下载模拟作答系统并登录后进行模拟练习,熟悉答题流程及操作方法。 一、模拟练习时间 2024年下半年软考机考模拟练习平台开放时间为2024年10月23日9:00至11月6日17:00,共15天。 考生可以在…...
合并.md文档
需求:将多个.md文档合并成一个.md文档。 方法一:通过 type 命令 参考内容:多个md文件合并 步骤: 把需要合并的 .md 文档放入到一个文件夹内。修改需要合并的 .md 文档名,可以在文档名前加上 1.2.3 来表明顺序&#x…...
10月18日笔记(基于系统服务的权限提升)
系统内核漏洞提权 当目标系统存在该漏洞且没有更新安全补丁时,利用已知的系统内核漏洞进行提权,测试人员往往可以获得系统级别的访问权限。 查找系统潜在漏洞 手动寻找可用漏洞 在目标主机上执行以下命令,查看已安装的系统补丁。 system…...
【STM32 Blue Pill编程实例】-控制步进电机(ULN2003+28BYJ-48)
控制步进电机(ULN2003+28BYJ-48) 文章目录 控制步进电机(ULN2003+28BYJ-48)1、步进电机介绍2、ULN2003步进电机驱动模块3、硬件准备及接线4、模块配置3.1 定时器配置3.2 ULN2003输入引脚配置4、代码实现在本文中,我们将介使用 STM32Cube IDE 使用 ULN2003 电机驱动器来控制28B…...
监督学习、无监督学习、半监督学习、强化学习、迁移学习、集成学习分别是什么对应什么应用场景
将对监督学习、无监督学习、半监督学习、强化学习、迁移学习和集成学习进行全面而详细的解释,包括定义、应用场景以及具体的算法/模型示例。 1. 监督学习 (Supervised Learning) 定义:监督学习是一种机器学习方法,其中模型通过已知的输入数…...
WSL2 Linux子系统调整存储位置
WSL2 默认不支持修改Linux 安装路径,官方提供的方式,只有通过导出、导入的方式实现Linux子系统的迁移。 修改注册表的方式官方不推荐,没有尝试过,仅提供操作方式(自行评估风险,建议备份好数据) 1. 打开 **注册表编辑器…...
Shiro授权
一、定义与作用 授权(Authorization),也称为访问控制,是确定是否允许用户/主体做某事的过程。在Shiro安全框架中,授权是核心组件之一,它负责控制用户对系统资源的访问权限,确保用户只能访问其被…...
算法题总结(十五)——贪心算法(下)
1005、K 次取反后最大化的数组和 给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组: 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。 以这种方式修改数组后,返回数组 可…...
《深度学习》【项目】自然语言处理——情感分析 <下>
目录 一、了解项目 1、任务 2、文件内容 二、续接上篇内容 1、打包数据,转化Tensor类型 2、定义模型,前向传播函数 3、定义训练、测试函数 4、最终文件格式 5、定义主函数 运行结果: 一、了解项目 1、任务 对微博评论信息的情感分…...
postgresql是国产数据库吗?
PostgreSQL不是国产数据库。但是PostgreSQL对国产数据库的发展有着重要影响,许多国产数据库产品是基于PostgreSQL进行二次开发的。 PostgreSQL的开源特性也是其受欢迎的重要原因之一。开源意味着任何人都可以查看、修改和使用PostgreSQL的源代码。这使得PostgreSQL…...
软考——计算机网络概论
文章目录 🕐计算机网络分类1️⃣通信子网和资源子网2️⃣网络拓扑结构3️⃣ 计算机网络分类3:LAN MAN WAN4️⃣其他分类方式 🕑OSI 和 TCP/IP 参考模型1️⃣OSI2️⃣TCP/IP🔴TCP/IP 参考模型对应协议 3️⃣OSI 和 TCP/IP 模型对应…...
01 设计模式-创造型模式-工厂模式
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一,它提供了一种创建对象的方式,使得创建对象的过程与使用对象的过程分离。 工厂模式提供了一种创建对象的方式,而无需指定要创建的具体类。 通过使用工厂模式…...
ComnandLineRunner接口, ApplcationRunner接口
ComnandLineRunner接口, ApplcationRunner接口 介绍: 这两个接口都有一个run方法,执行时间在容器对象创建好后,自动执行run ( )方法。 创建容器的同时会创建容器中的对象,同时会把容器中的对象的属性赋上值: 举例&…...
Swift用于将String拆分为数组的components与split的区别
根据特定分隔符拆分字符串 在 Swift 中,components(separatedBy:) 和 split(separator:) 都可以用于将字符串拆分为数组,但它们有一些关键区别。下面将从返回值类型、性能和功能等角度进行对比。 1. 返回值类型 components(separatedBy:):…...
docker之redis安装(项目部署准备)
创建网络 docker network create net-ry --subnet172.68.0.0/16 --gateway172.68.0.1 redis安装 #创建目录 mkdir -p /data/redis/{conf,data} #上传redis.conf文件到/data/redis/conf文件夹中 #对redis.conf文件修改 # bind 0.0.0.0 充许任何主机访问 # daemonize no #密码 # …...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
无人机侦测与反制技术的进展与应用
国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机(无人驾驶飞行器,UAV)技术的快速发展,其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统,无人机的“黑飞”&…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
