新闻详情

新闻详情

首页 / 资讯中心 / 详情

ArrayBlockingQueue源码解析:锁、Condition与生产实践

发布时间:2026/10/2 3:51:56来源:尧图网络
ArrayBlockingQueue源码解析:锁、Condition与生产实践
先说一个我自己的真实感受在 Java 面试里问“ArrayBlockingQueue 用过没”十个人有九个能背出来“有界阻塞队列、基于数组、FIFO”但真到线上排查生产问题的时候能说清楚它内部那套锁和条件是怎么协作的、为什么吞吐量上不去、什么时候该换 LinkedBlockingQueue 的人我遇到的确实不多。ArrayBlockingQueue 看起来简单但它的同步模型其实浓缩了并发编程中相当经典的一整套思路循环数组、可重入锁、条件队列、等待通知机制。搞懂它对理解整个 java.util.concurrent 包里的阻塞队列体系都有帮助而且它也是线程池、消息中间件、本地任务缓冲等场景里最常见的底层组件之一。这篇不打算写成流水账式的 API 手册我尽量以“源码 运行机制 生产踩坑”的视角把它拆开揉碎。适合正在准备并发相关的同学也适合那些已经用过它、但想弄明白“为什么有时候加锁了还是会有怪问题”的开发者。1. 一个锁、一张循环数组ArrayBlockingQueue 的核心存储模型在聊 API 之前建议先把它的“底子”看明白。ArrayBlockingQueue 之所以叫 Array是因为它内部就是用一个固定长度的 Object 数组来装元素的。这一点决定了它和 LinkedBlockingQueue 最根本的差异Array 是连续内存、固定容量Linked 是节点分散、链表式伸缩。1.1 循环数组指针怎么往前走先看三个关键字段的概念模型itemsObject[]存放元素的底层数组takeIndex下一次 take/peek 时读取的元素下标putIndex下一次 put/offer 时写入的元素下标count当前队列中的元素数量为什么说是“循环”数组因为 putIndex 和 takeIndex 在走到数组末尾时不会停住而是通过取模回到 0putIndex (putIndex 1) % items.length; takeIndex (takeIndex 1) % items.length;举个例子容量为 4 的队列放进 a、b、c、d 四个元素后 putIndex 回到 0这时再 remove 掉队头的 atakeIndex 从 0 走到 1。此时如果你再 put 一个 e它会写到下标 0 的位置把已经被消费掉的“坑位”重新利用起来。这个设计的价值在于出队操作不需要像 ArrayList 那样把后面所有元素往前搬移时间复杂度是 O(1)。只要 count 没满put 和 take 永远只操作各自的指针互相之间的“物理位置”没有冲突。1.2 锁和条件变量两个条件对应两个方向存储模型之外ArrayBlockingQueue 的并发控制模型才是精华。它内部只有一个 ReentrantLock以及由这把锁派生出的两个 Conditionfinal ReentrantLock lock; private final Condition notEmpty; private final Condition notFull; public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity 0) throw new IllegalArgumentException(); this.items new Object[capacity]; lock new ReentrantLock(fair); notEmpty lock.newCondition(); notFull lock.newCondition(); }这里有个很值得品味的细节为什么用两个 Condition而不是一个想象一下只用一把锁 一个条件变量的场景队列满了生产者在等队列空了消费者在等。如果只有一个条件变量生产者被唤醒时也许是因为消费者取走了元素但也有可能唤醒的是一个消费者比如队列空了消费者也在同一个条件上等待。这种“信号错乱”需要反复用 while 循环判断效率不高而且容易产生无意义的竞争。两个条件的好处是各等各的生产者只等“notFull”消费者只等“notEmpty”。当消费者 take 成功时准确唤醒一个等待在 notFull 上的生产者当生产者 put 成功时准确唤醒一个等待在 notEmpty 上的消费者。这样既避免了惊群也让语义更清晰。提示这两个 Condition 都是基于同一个 ReentrantLock 的所以“同时只能有一个线程持有锁”的约束并没有变。这个点后面讨论性能时会再展开。2. 构造参数里的细节容量、公平锁与初识 onEmpty构造一个 ArrayBlockingQueue 比很多人想象中更有讲究。构造函数一共有三个重载最常被忽略的是 fair 这个布尔参数。2.1 容量为什么必须显式指定和 LinkedBlockingQueue 不同ArrayBlockingQueue“天生”必须知道自己的上限。你没法创建一个“不限制容量”的 ArrayBlockingQueue因为底层数组的大小在构造时就固定了。容量传 0 会在构造阶段直接抛 IllegalArgumentException这一点常被用来做参数校验但实际业务中没人会传 0。比较常见的问题是容量设得过大或过小设小了生产者频繁阻塞设大了内存占用和 GC 压力上去了毕竟数组本身是连续分配的一段引用空间。2.2 fair 参数的代价被低估了public ArrayBlockingQueue(int capacity, boolean fair) public ArrayBlockingQueue(int capacity, boolean fair, Collection? extends E c)fair 为 true 时内部 ReentrantLock 走公平模式等待时间最长的线程优先获得锁。理论上这能避免线程饥饿但代价非常现实公平锁需要在线程被唤醒后重新检查队列中是否有更早排队的线程上下文切换和 CAS 操作的频率会明显上升吞吐量在竞争激烈时会明显下降。我自己在做本地任务缓冲中间件压测时对比过8 个生产者线程 8 个消费者线程队列容量 1024每轮放 10 万条任务fairtrue 的吞吐大约只有 fairfalse 的 60% 左右。除非你有强需求保证某个生产者或消费者不被长时间饿着否则默认 false 更符合绝大多数生产场景。2.3 一个容易忽略的初始化逻辑构造时预放元素第三个构造函数允许传入一个 Collection在构造时一次性放入初始元素。它内部会遍历集合并依次调用 enqueue如果集合元素个数超过容量会在插入到第 capacity1 个元素时抛出 IllegalStateException。这类构造函数实际用得不多但它有个隐藏特性传入的集合如果是无序的队列的初始顺序就完全取决于该集合迭代器的顺序。做测试的时候别想当然认为“我传的 Set 是什么顺序队列就是什么顺序”。2.4 性能和内存上的补充说明对象头 数组本身带来的开销容量为 N 的队列数组引用占 8N 字节压缩指针下 4N加上锁、两个条件、索引等固定开销。别小看这几个字段当你需要创建几千个队列实例比如按业务维度隔离队列时内存差就不是小数了。3. 核心 API 的阻塞语义put/take 与 offer/poll 的取舍逻辑ArrayBlockingQueue 常用的方法可以按“是否阻塞”分成几组。这一节我们重点把 put/take 和 offer/poll 的机制讲透顺便解释为什么很多开源框架更喜欢用带超时的 offer/poll。3.1 put 的内部流程等待与入队的完整链路看 put 的源码实现基于 JDK 8 到 21 的实现思路基本一致public void put(E e) throws InterruptedException { checkNotNull(e); final ReentrantLock lock this.lock; lock.lockInterruptibly(); try { while (count items.length) notFull.await(); enqueue(e); } finally { lock.unlock(); } }流程拆开看先检查元素是否为 null不允许 null 入队获取锁但是用的是 lockInterruptibly也就是说等待锁的过程中响应中断进入临界区后用一个 while 循环判断队列是否已满。如果满了就调用 notFull.await() 把当前线程挂起释放锁被唤醒后重新抢到锁再次检查 count items.length。记住这里必须是 while 而不是 if原因有两个一是 wait 可能被虚假唤醒spurious wakeup二是可能有多个生产者在等待某个消费者取走一个元素后notFull.signal() 只唤醒其中一个但被唤醒者需要重新确认队列到底还满不满队列有空位了执行 enqueueprivate void enqueue(E x) { final Object[] items this.items; items[putIndex] x; putIndex inc(putIndex); count; notEmpty.signal(); }enqueue 本身在锁保护下完成写入数组、移动 putIndex、count 自增最后唤醒一个等待中的消费者。这里有个很多人没想明白的问题为什么 enqueue 里要调 notEmpty.signal()而不是 take 方法里自己 signal 自己其实本质是——入队动作本身意味着“队列非空”这一状态发生了变化这个状态变化需要通知可能正在等待的消费者。在并发场景下通知谁、谁去唤醒谁讲究的是“状态变化由变更方广播”由 put 通知 notEmpty由 take 通知 notFull。3.2 take 的内部流程与 put 完全对称的镜像public E take() throws InterruptedException { final ReentrantLock lock this.lock; lock.lockInterruptibly(); try { while (count 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }dequeue 里做的事情是private E dequeue() { final Object[] items this.items; SuppressWarnings(unchecked) E x (E) items[takeIndex]; items[takeIndex] null; takeIndex inc(takeIndex); count--; notFull.signal(); return x; }注意这里把 takeIndex 位置的元素置为 null这一步很关键如果不置 null数组里会一直残留已出队元素的引用导致对象无法被 GC 回收。这在长生命周期的高容量队列上会造成内存泄漏。源码里几乎每个出队路径remove 方法、removeAt 方法、iterator.remove 等都会把对应位置清空这是 JDK 团队对内存安全非常执着的体现。3.3 offer/poll阻塞和非阻塞的边界控制offer 和 poll 本身不阻塞或只阻塞有限时间它们在实现上是 put/take 的“非阻塞版本”public boolean offer(E e) { checkNotNull(e); final ReentrantLock lock this.lock; lock.lock(); try { if (count items.length) return false; else { enqueue(e); return true; } } finally { lock.unlock(); } } public E poll() { final ReentrantLock lock this.lock; lock.lock(); try { return (count 0) ? null : dequeue(); } finally { lock.unlock(); } }这两个方法的特点是不抛 InterruptedException也不会让线程挂起。它们的价值在于“可控制失败”。举个很常见的生产场景一个任务提交服务你希望当队列满了之后能快速返回错误给上游而不是让上游线程一直阻塞避免线程堆积。这种场景用 put 就是灾难用 offer(e, timeout, TimeUnit) 或者 offer(e) 就恰到好处。带超时的 offer 实现多了一层循环等待public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { checkNotNull(e); long nanos unit.toNanos(timeout); final ReentrantLock lock this.lock; lock.lockInterruptibly(); try { while (count items.length) { if (nanos 0) return false; nanos notFull.awaitNanos(nanos); } enqueue(e); return true; } finally { lock.unlock(); } }值得提醒的是 awaitNanos 返回的是“剩余等待时间”可能由于各种原因被提前唤醒所以底层用 while 循环重新判断并在 nanos 0 时返回失败。这种写法是 Java 并发库的模板范式写自己的等待逻辑时也应该照这个来。3.4 peek/element 与 remove 的边界行为peek()返回队头元素但不移除队列为空时返回 nullelement()peek 的加强版空队列时抛 NoSuchElementExceptionremove(Object o)遍历查找并移除一个元素成功返回 trueremove(Object) 的实现比想象中复杂因为移除的并不总是队头元素可能出现在数组中间位置。看 removeAt 的实现时它需要处理“索引回绕”下的元素搬移最坏情况下要把从被删位置到 takeIndex 之间的所有元素整体前移一位。这绝对是 O(n) 操作性能不便宜线上慎用。4. 源码视角的竞争真相为什么它只有一个锁以及 “take 完唤醒 put” 的完整闭环很多人拿 ArrayBlockingQueue 和 LinkedBlockingQueue 对比后会冒出疑问ArrayBlockingQueue 只有一个锁生产者消费者会互相竞争同一把锁是不是性能上天然吃亏LinkedBlockingQueue 有 takeLock 和 putLock 两把锁是不是一定更快这个问题的答案比直觉复杂。这一节我们把竞争模型拆开说说 ArrayBlockingQueue 在什么情况下会暴露性能短板什么情况下其实足够好。4.1 两个条件、一个锁唤醒链的典型路径完整走一遍 3 个线程的场景线程 A生产者队列满时阻塞在 notFull线程 B消费者执行 take成功拿走元素count 从 max 变成 max-1线程 C生产者马上尝试 put发现 count max直接入队过程里 B 拿锁 - 出队 - notFull.signal() - 释放锁A 此时并不会立刻被唤醒继续执行而是要等 B 释放锁后去竞争同一把锁。也就是说虽然“notFull 被 signal”了A 也要参与锁竞争并且可能输给 C。这没有错这是正常竞争代价是 A 可能多等一会。但问题在另一个方向ArrayBlockingQueue 生产者消费者共享同一把锁意味着 put 和 take 之间的并发度不是真正的“读写并行”。如果用 LinkedBlockingQueueput 锁和 take 锁独立入队出队可以并行推进高并发下的整体吞吐确实更容易做高。4.2 单锁模型为何还能被广泛使用原因在于真正决定性能的往往不是锁的个数而是临界区的大小和竞争频率。ArrayBlockingQueue 的临界区极短入队就是写一个数组槽位、移动索引、count、signal出队就是读一个槽位、置 null、移动索引、count--、signal。锁竞争时间被压缩到极短所以大多数业务场景下单锁并不会成为明显瓶颈。真正让它吃亏的是“队列本身很短 操作频率极高”的组合。比如队列容量只有 1生产者消费者你来我往每次操作后 count 快速在 0 和 1 之间跳变这时所有 put/take 都在同一把锁上排队性能会显著劣化。反过来容量充足、操作不极端频繁ArrayBlockingQueue 的性能表现通常很不错而且因为它不需要额外创建节点对象GC 压力和内存碎片比 LinkedBlockingQueue 好。提示如果压测发现 ArrayBlockingQueue 成为瓶颈先检查队列容量设置是否过小再考虑换结构别一上来就“无脑换 LinkedBlockingQueue”。容量过小造成的竞争加剧换任何队列都救不了。4.3 弱一致迭代器读快照不是实时快照ArrayBlockingQueue 的迭代器是弱一致性的weakly consistent意思是迭代器创建后不会抛 ConcurrentModificationException但也不保证能实时看到后续新增的元素。IteratorE it queue.iterator(); queue.offer(x); while (it.hasNext()) { // 不一定能看到 x }它底层维护了一个 nextItem 引用和一套独立的索引推算逻辑利用内部数组和 count 在创建时刻的“快照”来推导遍历位置。遍历期间如果发生了元素搬移比如 removeAt 移动了元素迭代器可能漏掉或重复遍历部分元素。实际开发中我基本只在打印队列状态、做简单统计时用它的迭代器绝不依赖它做精确遍历和删除。4.4 批量操作的便捷与风险drainTo 的正确姿势drainTo 是处理批量消费最顺手的方法public int drainTo(Collection? super E c, int maxElements) { checkNotNull(c); if (c this) throw new IllegalArgumentException(); if (maxElements 0) return 0; final Object[] items this.items; final ReentrantLock lock this.lock; lock.lock(); try { int n Math.min(maxElements, count); int take takeIndex; int i 0; while (i n) { c.add((E) items[take]); items[take] null; take inc(take); i; } // 更新 count 等 return n; } finally { lock.unlock(); } }它一次性把最多 n 个元素转入目标集合全程持锁性能比单个 poll 循环好得多。注意两个坑第一目标集合不能是队列自身否则会抛 IllegalArgumentException第二它一次性转入 n 个元素后 count 直接减 n消费方如果用这块做“等待队列空了再暂停”的判断需要理解 drainTo 之后队列可能还有剩余元素注意返回值代表实际转移数量。5. 尺度与边界粒度问题、空元素禁令和 remove 的 O(n) 代价这一节集中聊“用 ArrayBlockingQueue 时容易吃暗亏”的细节。每个点都是我或身边同事在真实项目里踩过的。5.1 禁止 null 元素的真正原因ArrayBlockingQueue 在 put/offer/add 里第一行就是 checkNotNull(e)。为什么不允许 null一个直接原因是 poll 方法用 null 表示“队列为空”public E poll() { return (count 0) ? null : dequeue(); }如果允许 null 入队那 poll 返回 null 到底是“取到一个 null 元素”还是“队列为空”调用方无法区分。JDK 的阻塞队列实现ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等统一不允许 null就是为了把 null 当作“空/失败”信号保留下来。如果你的业务数据里确实会出现 null入队前先包装成 Optional 或者用一个哨兵对象包裹别想着绕开这个约束。5.2 removeAt 的元素搬移细节removeAt 的场景是删除一个指定下标的元素比如 remove(Object) 能删除中间某个元素。它有两段逻辑if (i takeIndex) { // 删除队头直接清空推进 items[takeIndex] null; takeIndex inc(takeIndex); } else { // 删除非队头元素需要把 i 到 takeIndex-1 之间的元素往前挪 for (int n i; n ! takeIndex; n dec(n)) { E e items[dec(n)]; items[n] e; } ... }最坏情况下要搬移几乎整个队列的元素复杂度 O(n)。如果一个队列容量很大、又频繁做中间元素删除这条路径会非常伤 CPU。线上排查时如果发现内存队列 CPU 飙升先看看是不是有人在用 remove(Object) 做“按 key 取消任务”。5.3 中断处理lockInterruptibly 与 await 的响应put/take 都声明了 throws InterruptedException使用阻塞语义时会响应中断。这意味着如果队列一直满/一直空线程挂在 await 上别的线程可以随时中断它。设计上有个点容易忽略中断发生时count 状态并没有改变所以被中断的 put/take 不会产生“半入队、半出队”的不一致状态。这一点由锁保证可以放心重试。5.4 内存可见性锁的边界就是内存屏障很多初学者会问ArrayBlockingQueue 里的 count 和数组元素不加 volatile为什么多线程下能看到最新值答案是所有读写都在同一把 ReentrantLock 的临界区内完成。ReentrantLock 基于 AbstractQueuedSynchronizerAQS其 lock/unlock 操作包含 volatile 状态的读写天然具备 happens-before 语义。所以在 ArrayBlockingQueue 里不需要额外给 items 或 count 加 volatile。这也是为什么源码里它们只是普通字段。这一点在阅读源码时非常关键不要看到普通字段就以为存在可见性问题要看它们是否被锁保护。5.5 慎用 contains 和 toArraycontains(Object o)需要遍历整个数组O(n)toArray()会创建一个新数组并复制元素同样 O(n)这两个方法在队列容量很大时都不便宜。我曾经遇到过一个业务每次任务提交前先 contains 一下判断“是否重复”队列容量 5000生产者 20 个线程直接导致 contains 成为热点。最后换上 ConcurrentHashMap 做去重标记才解决。阻塞队列擅长的是边界内的高效入出队不是查找。6. 选型与实战ArrayBlockingQueue、LinkedBlockingQueue 和场景匹配这大概是实际工作中被问得最多的问题到底用哪个我把二者的核心差异和选择标准整理一下。6.1 一个维度分清单锁与双锁的本质分歧维度ArrayBlockingQueueLinkedBlockingQueue底层结构循环数组固定容量单向链表节点容量可选无界/有界锁结构单锁 两个 ConditiontakeLock putLock两把锁内存分配初始化时一次性分配稳定每个元素一个 Node持续分配GC 有压力容量限制必须在构造时指定有界或无界无界时 put 永不阻塞但可能 OOM入队出队并发不能真正并行可取可放两把锁并行度更高中间元素删除支持但 O(n) 搬移支持链表删除 O(1)但需要遍历查找单从这个表看LinkedBlockingQueue 似乎全面占优但实际选型远没那么简单。因为 ArrayBlockingQueue 在“容量确定、任务生命周期短、内存敏感”的场景下有更好的稳定性和可预测性。6.2 建议先看场景再做决定生产者消费者数量都不多比如各 1~4 个流量平稳ArrayBlockingQueue 足够内存更省要求严格的容量上限避免无界增长引发的 OOMArrayBlockingQueue 更直观容量显式写死对批量消费drainTo有较高依赖两者都能用ArrayBlockingQueue 的批量转储由于连续数组更高效非常高的吞吐需求、大量线程竞争可以先试试 LinkedBlockingQueue 的双锁并行压测对比再定任务可能偶尔积压且不想丢数据选有界队列的容量要大并结合特征评估拒绝策略6.3 一个可复用的 FastFail 写流控方案实际项目里我经常用 offer 超时实现“软限流”ExecutorService executor new ThreadPoolExecutor( core, max, keepAlive, unit, new ArrayBlockingQueue(queueSize) ); // 提交任务时 if (!executor.getQueue().offer(task, 500, TimeUnit.MILLISECONDS)) { // 入队失败走降级逻辑丢弃或返回提示 return handleReject(task); }这样队列满时提交线程最多阻塞 500ms不会无限挂起也不会像直接 put 那样可能把上游线程全部拖死。对于需要快速失败、保护系统的场景这比单纯用饱和策略更可控。7. 我踩过的几个坑与最后的调参心得最后分享几个我实际调试过的 Case每一个都对应上面某个机制希望能帮你少走弯路。第一个坑是“队列容量设为 1”。当时做实时数据管道两个线程互相传递消息我图省事把容量设成 1结果性能惨不忍睹。后来用 JFR 一看几乎所有时间都耗在锁竞争和条件唤醒上。把容量调到 16 之后吞吐翻了好几倍。原因是容量太小时生产者一入队消费者立即被唤醒但消费者抢到锁之前生产者又想继续入队造成两个线程争同一把锁的频率急剧上升。第二个坑是“用 remove(Object) 取消待处理任务”。系统里有一批延迟任务消费者还没处理前如果收到撤销信号就 remove。队列容量 2000每天跑批某天突然 CPU 飙到 90%排查后发现是 remove(Object) 触发了大量元素搬移把原本 O(1) 的操作变成了近似 O(n)。后来改成每个任务维护一个状态标记消费者处理前先判断状态是否被取消彻底摆脱了对中间元素删除的依赖。第三个坑是“把队列当消息中心用”。曾经有人在业务里用 ArrayBlockingQueue 做跨模块的异步通信但队列只是进程内的结构不具备持久化、重试、多副本能力。应用重启后积压在队列里的任务全丢了。阻塞队列适合做线程间沟通不适合做跨服务或跨重启的可靠消息通道这个边界要拎清。至于调参心得先根据自己的业务模型算出“峰值积压量”再乘以一个 1.5~2 的安全系数作为容量。别拍脑袋定大小。容量定得太小会让生产者频繁阻塞定得太大又浪费内存、拖慢遍历类操作。真实场景里ArrayBlockingQueue 的容量和生产者消费者线程数是强耦合的最好在压测环境多测几组参数用数据说话。最后一个很小的技巧但很实用队列的 toString 方法会一次性拼接所有元素生产环境千万别在日志里随手打 queue.toString()。曾经有个同事在告警日志里打印了容量 10000 的队列内容结果告警系统直接被打爆拼接一万条字符串的耗时也远超预期。真要观察队列状态用 size()、remainingCapacity() 这类 O(1) 方法就足够了。ArrayBlockingQueue 的源码看过一遍之后你会发现它其实是并发编程最好的“教科书案例”之一锁、条件、循环数组、通知唤醒、内存可见性全都浓缩在这一个类里。把它吃透再回去看线程池的 workQueue 参数、看各种消息中间件的本地缓存层设计思路会通透很多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

浏览器取证神器hindsight:Chrome/Chromium痕迹解析实战指南 2026/10/2 4:52:22

浏览器取证神器hindsight:Chrome/Chromium痕迹解析实战指南

说到浏览器取证,很多做 DFIR 的朋友第一个会想到的就是 hindsight。这个由 Ryan Benson 维护的开源项目,专门用来解析 Google Chrome / Chromium 以及各类基于 Chromium 内核的浏览器(Edge、Brave 都算)的本地数据。简单说&#x…

阅读更多 →
开源项目趋势周刊第315期:如何练就筛选优质开源项目的火眼金睛 2026/10/2 4:52:22

开源项目趋势周刊第315期:如何练就筛选优质开源项目的火眼金睛

1. 先说清楚:这份周刊到底在追什么做开源项目趋势追踪这件事,我大概持续关注了六七年。从最初自己翻 GitHub Trending,到后来固定在每周整理一份开源项目趋势周刊,第315期这个编号听起来挺唬人,其实就是每周一期的常规…

阅读更多 →
Laya引擎下雨效果实战:从粒子系统到屏幕氛围的全流程解析 2026/10/2 4:52:22

Laya引擎下雨效果实战:从粒子系统到屏幕氛围的全流程解析

做天气系统是我在游戏开发里最喜欢碰的一类需求,尤其是下雨天。它不像战斗系统那样逻辑复杂,也不像UI那样琐碎,但一个高质量的下雨效果能让整个场景的质感瞬间上一个台阶。这篇文章就完整记录一下我在Laya引擎里落地“下雨环境效果”的全过程…

阅读更多 →
15MB小工具统一管理Codex与Claude Code模型配置 2026/10/2 4:52:22

15MB小工具统一管理Codex与Claude Code模型配置

1. 这个 15MB 的小工具到底解决了什么问题1.1 从两个真实痛点说起如果你同时用 Codex 和 Claude Code 这两个命令行 AI 编程助手,大概率遇到过这种场景:早上用 Codex 连着某个模型写代码,下午想切到 Claude Code 换个模型跑任务,结…

阅读更多 →
统一管理54+AI编程工具Agent技能:Skills Manager跨平台中枢实战 2026/10/2 4:52:15

统一管理54+AI编程工具Agent技能:Skills Manager跨平台中枢实战

不再多绕圈子了,今天聊一个我最近折腾了很久的实战项目:Skills Manager——一个统一管理 54 AI 编程工具 Agent 技能的跨平台桌面中枢。这件事的起因很简单:我的日常开发工作已经离不开各类 AI 编程 Agent(从 Cursor、Copilot 到开…

阅读更多 →
从像素图到3D场景:Blender+Codex+Unity/Godot全流程还原未白镇 2026/10/2 4:52:15

从像素图到3D场景:Blender+Codex+Unity/Godot全流程还原未白镇

1. 从一张像素图到可跑可跳的3D场景,这条路我走通了「未白镇」这三个字,玩过经典像素RPG的人应该都不陌生——那个开局的小镇,几间矮房、一条土路、几棵树、一块告示牌,画面简单到只有几十个像素点,却承载了无数人的童…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉