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

AQS源码解析

关于 AQS,网上已经有无数的文章阐述 AQS 的使用及其源码,所以多这么一篇文章也没啥所谓,还能总结一下研究过的源码。源码解析和某某的使用,大概是互联网上 Java 文章中写得最多的主题了。

AQS

AQS 是 AbstractQueuedSynchronizer 的缩写,中文翻译过来就是抽象队列同步器。ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatch 都是基于 AQS。AQS 的核心思想是,当线程请求获取资源时,如果资源空闲,则会将当前线程设置为资源的独占线程,成功获得锁;否则将获取锁失败的线程加入到排队队列中(CLH),并提供线程阻塞和线程唤醒机制。CLH 是一个虚拟的双向队列。

首先看一下 AQS 的关键属性。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private transient volatile Node head; //队列的头节点
private transient volatile Node tail; //队列的尾节点
private volatile int state; //同步状态

state 用于实现锁的可重入性。

  • 0 为表示没有线程持有该锁
  • 当存在线程获取锁成功,则 state 变为 1。如果是同一线程重复获得,则 state++
  • 如果存在线程释放锁,则 state–

上面的三个属性都存在对应的以 CAS 方式进行修改的方法,state 对应的是 compareAndSetState() 方法, head 对应的是 compareAndSetHead() 方法,tail 对应的是 compareAndSetTail()。以 CAS 的方式修改值能避免锁的竞争。

因为请求获取锁的线程会以 Node 节点的方式在 CLH 队列中排队,在分析 AQS 机制时也会大量涉及到 Node 节点,所以很有必要对 Node 节点进行分析。

CLH 队列中 Node 节点。

// java.util.concurrent.locks.AbstractQueuedSynchronizer.Node
volatile int waitStatus;  //当前节点在队列中的状态
volatile Node prev;  //前驱节点
volatile Node next;  //后继节点
volatile Thread thread;  //在该节点中排队的线程
Node nextWaiter;  //下一个处于condition或共享状态的节点//等待锁的两种状态
static final Node SHARED = new Node();  //共享
static final Node EXCLUSIVE = null; //独占//waitStatus的几个值 
static final int CANCELLED =  1;  //已取消
static final int SIGNAL    = -1;  //后继节点的线程需要唤醒
static final int CONDITION = -2;  //节点处于等待队列中
static final int PROPAGATE = -3; //线程处在SHARED情况下使用该字段

ReentrantLock

说是研究 AQS 源码,但 AQS 毕竟是一个抽象类,只实现了部分方法,另外一些方法会在子类中实现。所以我们也同样会涉及到 ReentrantLock 源码的研究。

ReentrantLock 的通常使用方式是:

class X {    private final ReentrantLock lock = new ReentrantLock();       public void m() {      lock.lock();       try {        // ... method body      } finally {        lock.unlock()      }    }  
}

ReentrantLock 存在两种锁:公平锁(FairSync)和非公平锁(NonfairSync),默认为非公平锁。使用公平锁时,线程会直接进入队列中排队,只有队列中第一个线程才能获取锁;使用非公平锁时,线程会先尝试获取锁,成功则占用锁,失败则在队列排队。对于 AQS 来说,公平锁和非公平锁的绝大部分方法都是共用的。

ReentrantLock 的主要方法有两个,一是使用 lock() 方法加锁,二是使用 unlock() 方法解锁。

加锁

非公平锁

我们首先来分析非公平锁的加锁过程。

// java.util.concurrent.locks.ReentrantLock.NonfairSync
final void lock() {  if (compareAndSetState(0, 1))  //通过cas方式设置同步状态setExclusiveOwnerThread(Thread.currentThread());  //成功,设置为独占线程else        acquire(1);  //失败,获取锁
}

lock() 方法中,会先尝试通过 CAS 的方式去获取锁,成功则设置为独占线程,否则执行 acquire() 方法。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {  if (!tryAcquire(arg) &&  //尝试获取锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 失败,则添加到等待队列selfInterrupt();  
}

acquire() 方法中,会再次尝试去获取锁,如果失败则加入到排队队列。

// java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {  return nonfairTryAcquire(acquires);  
}// java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {  final Thread current = Thread.currentThread();  int c = getState();  if (c == 0) {  //如果当前没有线程持有锁if (compareAndSetState(0, acquires)) {  //state++setExclusiveOwnerThread(current);  //将当前线程设置为独占线程return true;        }  }  else if (current == getExclusiveOwnerThread()) {  //如果已有线程持有锁,且当前线程为独占线程int nextc = c + acquires;  if (nextc < 0) // overflow  throw new Error("Maximum lock count exceeded");  setState(nextc);   //state++,实现锁的可重入性return true;   //获得锁}  return false;  //如果已有线程持有锁,且当前线程不为独占线程,则获取锁失败
}

nonfairTryAcquire() 方法中,如果资源空闲,则再次尝试通过 CAS 的方式去获取锁。如果当前资源已被当前线程占用,则将 state++,以实现锁的可重入性。

// java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
// 设置队列尾节点
private Node addWaiter(Node mode) {  Node node = new Node(Thread.currentThread(), mode);   //新建排队节点 Node pred = tail;  //pred指向尾节点if (pred != null) {  //如果Pred指针是Null(说明等待队列中没有元素),或者当前Pred指针和Tail指向的位置不同(说明被别的线程已经修改)node.prev = pred;  //将新建节点的prev指向predif (compareAndSetTail(pred, node)) {  // 设置新建节点为尾节点pred.next = node;  return node;  }  }  enq(node);  return node;  
}//java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
private Node enq(final Node node) {  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() 方法中会将线程放到节点中,并设置为排队队列的尾节点。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
final boolean acquireQueued(final Node node, int arg) {  //是否成功获取到资源boolean failed = true;  try {  //循环等待过程中是否中断过boolean interrupted = false;  for (;;) {  final Node p = node.predecessor();  //获取当前节点的前驱节点if (p == head && tryAcquire(arg)) {  //如果前驱节点为头节点,则尝试获取锁setHead(node);  //获取锁成功,设置当前节点为头节点p.next = null; // help GC  failed = false;  return interrupted;  }  //来到这里,说明前驱节点不是头节点或者获取锁失败。判断当前节点是否要被阻塞if (shouldParkAfterFailedAcquire(p, node) &&  parkAndCheckInterrupt())  interrupted = true;  //线程中断成功}  } finally {  if (failed)  cancelAcquire(node);  }  
}

加入到排队队列中后,会在 acquireQueued() 方法中循环等待资源的获取,并判断线程是否需要被阻塞,直到线程获取成功或者抛出异常。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
//靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  //获取前驱节点的状态int ws = pred.waitStatus;  if (ws == Node.SIGNAL)  //如果前驱节点处于唤醒状态  return true;  if (ws > 0) {  //前驱节点处于取消状态      do {  //向前查找取消的节点,并将其从队列中删除node.prev = pred = pred.prev;  } while (pred.waitStatus > 0);  pred.next = node;  } else {  compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  //设置前驱节点为唤醒状态}  return false;  
}
//java.util.concurrent.locks.AbstractQueuedSynchronizer#parkAndCheckInterrupt
//挂起当前线程,阻塞调用栈,返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {  LockSupport.park(this);  return Thread.interrupted();  
}

整一个非公平锁的加锁流程可以用如下流程图表示:

成功
失败
失败
队列中的线程去获取锁
true
false
设置当前节点为头节点
如果当前节点的前驱节点为头节点且获取锁成功
返回是否被中断过
当前节点是否需要被阻塞
阻塞当前线程
将当前节点添加到队列
不为空
为空
设置当前节点为尾节点
尾节点是否为空
返回当前节点
enq
为空
不为空
尾节点是否为空
初始化一个空的头节点
设置当前节点为尾节点
返回当前节点
lock
cas获取锁
设置为独占线程
尝试获取锁

公平锁

公平锁和非公平锁的流程中只有 lock() 方法和 tryAcquire() 方法存在差别。

//java.util.concurrent.locks.ReentrantLock.FairSync#lock
final void lock() {  acquire(1);  //直接获取锁
}

公平锁中会直接调用 acquire() 方法。

// java.util.concurrent.locks.ReentrantLock.FairSync#tryAcquire
protected final boolean tryAcquire(int acquires) {  final Thread current = Thread.currentThread();  int c = getState();  if (c == 0) {  if (!hasQueuedPredecessors() &&  compareAndSetState(0, acquires)) {  setExclusiveOwnerThread(current);  return true;            }  }  else if (current == getExclusiveOwnerThread()) {  int nextc = c + acquires;  if (nextc < 0)  throw new Error("Maximum lock count exceeded");  setState(nextc);  return true;        }  return false;  
}  

相对于 nonfairnonfairTryAcquire 方法,在没有线程持有锁时,增加了 hasQueuedPredecessors() 方法的判断,该方法用于判断队列中是否存在线程比当前线程等待时间更长。

// java.util.concurrent.locks.AbstractQueuedSynchronizer#hasQueuedPredecessors
public final boolean hasQueuedPredecessors() {     Node t = tail; Node h = head;  Node s;  return h != t &&  ((s = h.next) == null || s.thread != Thread.currentThread());  
}

分析一下这段代码:

  • 如果 h != t 为 true,说明队列正在初始化,或者已经初始化完成。
    • h != t 前提下,如果 (s = h.next) == null 为 true,说明队列正在初始化,因为在队列初始化过程中,是有可能存在 head 已经被初始化(不再等于 tail 了),但 head.next 还没有被设值为 node,这种情况下,因为队列中已经存在 Node,当前线程需要加到等待队列中,故返回 true。初始化过程在 AbstractQueuedSynchronizer#enq()
    • 但如果 (s = h.next) == null 为 false,说明队列中已经存在 Node,则判断该 Node 的线程是否与当前线程相同。如果 s.thread != Thread.currentThread() 为 true,说明不相同,需要进入等待队列。如果相同,说明当前线程可以获取锁。
  • 如果 h != t 为 false,说明队列为空,返回 false,说明可以去获得锁。

另外,s = h.next 这段代码获取的是 head 的下一个节点,因为 head 是虚节点,不存储数据,真正的数据存储在 head.next

解锁

相对于加锁过程,解锁过程比较简单,且公平锁和非公平锁共用同一个 lock() 方法。

// java.util.concurrent.locks.ReentrantLock#unlock
public void unlock() {  sync.release(1);  
}//java.util.concurrent.locks.AbstractQueuedSynchronizer#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;  
}

关于代码 h != null && h.waitStatus != 0,分为以下几种情况:

  1. h==null,说明头节点还未初始化。
  2. h!=null && h.waitStatus==0,说明后继节点还在运行中。因为如果节点已被取消,waitStatus 会被设置为 1;如果后继节点需要唤醒,waitStatus 会被设置为 -1;如果节点正在排队,waitStatus 则会设置为 -2。如果 waitStatus 为 0,说明已经处于运行中。
  3. h != null && h.waitStatus != 0 则会去唤醒后继节点。

release() 方法中,会先尝试去解锁,如果解锁成功且后继节点需要唤醒,则将后继节点取消挂起。

//java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
//更新state状态,如果重入次数为0,则将锁的独占线程设置为null
protected final boolean tryRelease(int releases) {  //减少可重入次数int c = getState() - releases;  if (Thread.currentThread() != getExclusiveOwnerThread())  //如果当前线程不是该锁的独占线程,则抛出异常throw new IllegalMonitorStateException();  boolean free = false;  //该锁是否已被释放if (c == 0) {  //如果可重入次数已为0free = true;  setExclusiveOwnerThread(null);  //将锁的独占线程设置为null,表明不再有线程持有该锁}  setState(c);  //修改锁的状态return free;  
}

tryRelease() 方法中,会去减少锁的可重入次数,当可重入次数为 0 时,清空锁的独占线程。

// java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor
private void unparkSuccessor(Node node) {     int ws = node.waitStatus;  //获取头节点的waitStatusif (ws < 0)  //如果节点的waitStatus为负数,说明后继节点需要被唤醒或者正在排队compareAndSetWaitStatus(node, ws, 0);  //清除节点当前的waitStatus,设置为0  Node s = node.next;  //获取头节点的后继节点if (s == null || s.waitStatus > 0) {  //如果节点为null,或者已被取消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);  
}

unparkSuccessor() 方法中,会从尾节点开始从后往前遍历,找到队列中第一个没有被取消的节点,将该节点取消挂起。

整个解锁流程可以用如下流程图表示。

解除后继节点的挂起
清除该节点的waitStatus状态
如果头节点需要被唤醒
如果后继节点为null或者后继节点已被取消
从尾节点开始往前遍历找到第一个不被取消的节点
如果找到了不被取消的节点则取消该节点的挂起
尝试释放锁
该线程是否持有该锁
减少可重入次数
抛出异常
可重入次数是否为0
设置该锁的独占线程为null并释放掉锁
修改该锁的状态
如果释放锁成功且后继节点需要解除挂起

相关文章:

AQS源码解析

关于 AQS&#xff0c;网上已经有无数的文章阐述 AQS 的使用及其源码&#xff0c;所以多这么一篇文章也没啥所谓&#xff0c;还能总结一下研究过的源码。源码解析和某某的使用&#xff0c;大概是互联网上 Java 文章中写得最多的主题了。 AQS AQS 是 AbstractQueuedSynchronize…...

关于在VS2017中编译Qt项目遇到的问题

关于在VS2017中编译Qt项目遇到的问题 【QT】VS打开QT项目运行不成功 error MSB6006 “cmd.exe”已退出,代码为 2。如何在VS2017里部署的Qt Designer上编辑槽函数 【QT】VS打开QT项目运行不成功 error MSB6006 “cmd.exe”已退出,代码为 2。 链接 如何在VS2017里部署的Qt Design…...

Python web实战 | 使用 Flask 实现 Web Socket 聊天室

概要 今天我们学习如何使用 Python 实现 Web Socket&#xff0c;并实现一个实时聊天室的功能。本文的技术栈包括 Python、Flask、Socket.IO 和 HTML/CSS/JavaScript。 什么是 Web Socket&#xff1f; Web Socket 是一种在单个 TCP 连接上进行全双工通信的协议。它是 HTML5 中的…...

Android10 Recovery系列(一)隐藏recovery菜单项

一 、背景 起因是遇到了一个隐藏删除recovery菜单项的需求。在寻找解决问题的时候,我经历了找到源码位置,调试修改,生效,思考是否可拓展,优化修改,符合要求的整个过程,下面简单分享一下。如果不想立即实现效果或者只想看解决方案,可以直接看总结那一个部分 二 、准备…...

选好NAS网络储存解决方案,是安全储存的关键

随着网络信息的发展&#xff0c;NAS也越来越受到企业的关注&#xff0c;NAS网络存储除了提供简单的存储服务外&#xff0c;还可以提供更好的数据安全性、更方便的文件共享方式。但市面上的产品种类繁多&#xff0c;我们该如何选择合适的产品&#xff0c;通过企业云盘&#xff0…...

AnimateDiff论文解读-基于Stable Diffusion文生图模型生成动画

文章目录 1. 摘要2. 引言3. 算法3.1 Preliminaries3.2. Personalized Animation3.3 Motion Modeling Module 4. 实验5.限制6. 结论 论文&#xff1a; 《AnimateDiff: Animate Your Personalized Text-to-Image Diffusion Models without Specific Tuning》 github: https://g…...

centos7安装tomcat

安装tomcat 必须依赖 JDK 环境&#xff0c;一定要提前装好JDK保证可以使用 一、下载安装包 到官网下载 上传到linux 服务器 二、安装tomcat 创建tomcat 文件夹 mkdir -p /usr/local/tomcat设置文件夹权限 chmod 757 tomcat将安装包上传至 新建文件夹 解压安装包 tar zx…...

【C#教程】零基础从入门到精通

今天给大家分享一套零基础从入门到精通&#xff1a;.NetCore/C#视频教程&#xff1b;这是2022年最新整理的、590G的开发教程资料。课程涵盖了.Net各方面的知识&#xff0c;跟着这个教程学习&#xff0c;就足够了。 课程分类 1、C#从基础到精通教程&#xff1b; 2、Winform从…...

opencv rtsp 硬件解码

讨论使用opencv的reader 硬件解码的方案有太多种&#xff0c;如果使用ffmpeg硬件解码是最方便的&#xff0c;不方便的是把解码过后的GPU 拉到 CPU 上&#xff0c;再使用opencv的Mat 从cpu 上上载到gpu上&#xff0c;是不是多了两个过程&#xff0c;应该是直接从GPU mat 直接去…...

机器学习-Gradient Descent

机器学习(Gradient Descent) videopptblog 梯度下降(Gradient Descent) optimization problem: 损失函数最小化 假设本模型有两个参数&#x1d703;1和&#x1d703;2&#xff0c;随机取得初始值 求解偏微分&#xff0c;梯度下降对参数进行更新 Visualize: 确定梯度方向&…...

MySql003——SQL(结构化查询语言)基础知识

一、数据库的相关概念 DB&#xff1a;数据库&#xff08;Database&#xff09; 即存储数据的“仓库”&#xff0c;其本质是一个文件系统。它保存了一系列有组织的数据。DBMS&#xff1a;数据库管理系统&#xff08;Database Management System&#xff09; 是一种操纵和管理数…...

springCloud Eureka注册中心配置详解

1、创建一个springBoot项目 2、在springBoot项目中添加SpringCloud依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>2021.0.3</version><type>…...

gti 远程操作

目录 一. 分布式版本控制管理系统 1. 理解分布式版本控制管理系统 二. 创建远程仓库 ​编辑 ​编辑 三. 克隆远程仓库_HTTP 四. 克隆远程仓库_SSH 配置公钥 添加公钥 五. git 向远程仓库推送 六. 拉取远程仓库 七. 忽略特殊文件 八. 配置别名 一. 分布式版本控制管理…...

Ftrace

一、概述 Ftrace有剖析器和跟踪器。剖析器提供统计摘要&#xff0c;如激素胡和直方图&#xff1b;而跟踪器提供每一个事件的细节。 Ftrace剖析器列表&#xff1a; 剖析器描述function内核函数统计分析kprobe profiler启用的kprobe计数器uprobe profiler启用的uprobe计数器hi…...

Tomcat修改端口号

网上的教程都比较老&#xff0c;今天用tomcat9.0记录一下 conf文件夹下server.xml文件 刚开始改了打红叉的地方&#xff0c;发现没用&#xff0c;改了上面那行...

vue2企业级项目(一)

vue2企业级项目&#xff08;一&#xff09; 创建项目&#xff0c;并创建项目编译规范 1、node 版本 由于是vue2项目&#xff0c;所以 node 版本比较低。使用 12.18.3 左右即可 2、安装vue 安装指定版本的vue2 npm i -g vue2.7.10 npm i -g vue/cli4.4.63、编辑器规范 vsc…...

【前端知识】React 基础巩固(三十八)——log、thunk、applyMiddleware中间件的核心代码

React 基础巩固(三十八)——log、thunk、applyMiddleware中间件的核心代码 一、打印日志-中间件核心代码 利用Monkey Patching&#xff0c;修改原有的程序逻辑&#xff0c;在调用dispatch的过程中&#xff0c;通过dispatchAndLog实现日志打印功能 // 打印日志-中间件核心代码…...

hive删除数据进行恢复

在实际开发或生产中&#xff0c;hive表如果被误删&#xff0c;如被truncate或是分区表的分区被误删了&#xff0c;只要在回收站的清空周期内&#xff0c;是可以恢复数据的&#xff0c;步骤如下&#xff1a; &#xff08;1&#xff09; 先找到被删除数据的存放目录&#xff0c;…...

二、前端高德地图、渲染标记(Marker)引入自定义icon,手动设置zoom

要实现这个效果&#xff0c;我们先看一下目前的页面展示&#xff1a; 左边有一个图例&#xff0c;我们可以方法缩小地图&#xff0c;右边是动态的marker标记&#xff0c;到时候肯定时候是后端将对应的颜色标识、文字展示、坐标点给咱们返回、我们肯定可以拿到一个list&#xf…...

UDF和UDAF、UDTF的区别

UDF UDF&#xff08;User-defined functions&#xff09;用户自定义函数&#xff0c;简单说就是输入一行输出一行的自定义算子。 是大多数 SQL 环境的关键特性&#xff0c;用于扩展系统的内置功能。&#xff08;一对一&#xff09; UDAF UDAF&#xff08;User Defined Aggregat…...

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...

生成xcframework

打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式&#xff0c;可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

python打卡day49

知识点回顾&#xff1a; 通道注意力模块复习空间注意力模块CBAM的定义 作业&#xff1a;尝试对今天的模型检查参数数目&#xff0c;并用tensorboard查看训练过程 import torch import torch.nn as nn# 定义通道注意力 class ChannelAttention(nn.Module):def __init__(self,…...

大话软工笔记—需求分析概述

需求分析&#xff0c;就是要对需求调研收集到的资料信息逐个地进行拆分、研究&#xff0c;从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要&#xff0c;后续设计的依据主要来自于需求分析的成果&#xff0c;包括: 项目的目的…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

【大模型RAG】Docker 一键部署 Milvus 完整攻略

本文概要 Milvus 2.5 Stand-alone 版可通过 Docker 在几分钟内完成安装&#xff1b;只需暴露 19530&#xff08;gRPC&#xff09;与 9091&#xff08;HTTP/WebUI&#xff09;两个端口&#xff0c;即可让本地电脑通过 PyMilvus 或浏览器访问远程 Linux 服务器上的 Milvus。下面…...

Golang dig框架与GraphQL的完美结合

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

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)

🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...