Java并发基石AQS:源码拆解锁与队列的底层原理
1. 别急着看代码先搞懂AQS到底在解决什么麻烦我在读Java并发源码的时候第一感受是为什么一个叫AbstractQueuedSynchronizer的抽象类能撑起几乎整个Java并发工具包的半边天。不信你翻开JDK看看ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock底层清一色都是站在AQS的肩膀上。所以标题里说它是并发锁的基石一点都不夸张。但真正让我对AQS刮目相看的是我第一次尝试自己写一个可重入锁的失败经历。当时我用synchronized加一颗全局状态变量再用wait/notify做阻塞唤醒写着写着发现两个问题绕不过去一是多个线程竞争同一个锁时怎么保证只有一个线程能改成功状态二是没抢到锁的线程去哪里排队排队之后被唤醒的时机怎么把控如果每个锁实现都自己重新发明一套阻塞队列和CAS逻辑线程多了之后光是这些小轮子就会变成大灾难。AQS做的事情本质上是把锁的通用骨架抽出来。它不管你实现的是公平锁还是非公平锁也不管你是独占锁还是共享锁它只操心两件事一堆线程抢一个状态位抢不到的线程怎么安全地等待锁释放的时候怎么公平地挑一个等待者唤醒。这两件事在并发编程里属于通用难题而在AQS里被一次性解决干净了。所以这篇文章不是从ReentrantLock的API讲起而是直接钻进AQS源码内部把state、CLH队列、模板方法这些核心机制一条条拆开。看完之后你不但能理解AQS本身还能顺带看穿所有基于它实现的并发工具面试时被问到AQS底层原理也不再是背概念而是能讲出完整链路。2. 三个核心构件state、CLH队列与模板方法设计2.1 state一把锁的原子状态账本打开AQS源码你会先看到一个字段private volatile int state;这个volatile int是整个同步器的灵魂。它的含义由子类自己定义在ReentrantLock里它表示锁被重入了几次0代表无锁大于0代表某个线程持有了锁在Semaphore里它表示剩余的许可证数量在CountDownLatch里它表示还需要等待多少个事件。注意它被声明为volatile意味着任何线程对此字段的写入都会立刻对其他线程可见。但仅靠volatile还不够——多个线程同时执行state state - 1这种操作时会有竞态条件所以AQS配套提供了三个基于CAS的方法compareAndSetState(int expect, int update)经典的CAS更新getState()读取当前状态值setState(int newState)普通写操作通常用在持锁线程自己修改状态的场景比如重入计数你去看ReentrantLock的非公平锁加锁代码第一行就是CAS尝试final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }这一步的意图非常直白我先试试能不能把state从0改成1能改成就说明抢锁成功直接设置持有线程改不成就说明有人占着于是走acquire(1)进入排队流程。CAS是CPU提供的原子指令性能比加锁做状态变更高得多这也是AQS效率的一个重要来源。2.2 CLH变体队列公平排队背后的数据结构光有state还不够。当多个线程CAS抢锁失败后它们需要一个等待区AQS选择了一条双向链表也就是常说的CLH锁的变体。原始CLH锁是一个隐式的单向链表每个等待者自旋轮询前驱节点的状态而AQS做了改进节点中保存真实线程对象节点之间有prev和next双向指针并引入两种关键状态字段static final int CANCELLED 1; static final int SIGNAL -1; static final int CONDITION -2; static final int PROPAGATE -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread;队列里有一个哨兵节点作为头节点head它的thread字段通常为null不代表任何等待线程真正排队的是从头节点之后的一个个节点每个节点承载一个因抢锁失败而挂起的线程。整个结构大致长这样head - node1 - node2 - ... - tail。为什么用这种队列而不是一把大锁串起所有等待线程因为在等待锁的场景里一个线程只需要关注自己的前驱节点状态就够了。前驱节点被唤醒并释放锁后会主动唤醒后继节点。这种一节一节往前递的接力方式把唤醒操作局部化避免了所有线程同时被唤醒造成无意义竞争。这正是CLH队列的核心思想。2.3 模板方法模式框架把流程写死子类只填策略AQS最妙的设计之一是它用模板方法模式把加锁/解锁的流程骨架固定下来把什么样的状态算获取成功这种策略留给子类去实现。也就是说AQS不关心你拿锁做的是什么业务它只负责管理等待队列和阻塞唤醒。AQS暴露给子类重写的方法有五个方法作用默认行为tryAcquire(int arg)独占方式尝试获取资源抛异常tryRelease(int arg)独占方式尝试释放资源抛异常tryAcquireShared(int arg)共享方式尝试获取资源抛异常tryReleaseShared(int arg)共享方式尝试释放资源抛异常isHeldExclusively()当前线程是否为独占持有者返回false子类必须实现至少其中一部分方法才能让AQS跑起来。比如ReentrantLock实现了tryAcquire和tryReleaseSemaphore实现了tryAcquireShared和tryReleaseShared。AQS内部不可重写的acquire(int)、release(int)等方法则负责完成尝试获取、失败入队、挂起线程、释放后唤醒来一整套通用流程。这个设计把同步器到底是什么和怎么管理等待线程彻底解耦。你要自定义一把锁只需要用state定义自己的逻辑再实现对应的try方法即可。这也是为什么AQS能派生出这么多看似毫不相关的并发工具的根本原因。3. 加锁之路从acquire到tryAcquire的完整链路3.1 acquire的整体骨架三件套流程独占模式获取锁的入口是acquire(int arg)源码非常短但信息密度极高public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这里做了三件事先尝试tryAcquire(arg)再抢一次锁抢不到就调用addWaiter(Node.EXCLUSIVE)把当前线程包装成独占模式节点加入队尾然后进入acquireQueued在队列里自旋或阻塞地等待。如果acquireQueued返回true说明等待过程中线程序曾被中断标记但真正拿到锁之后才补上selfInterrupt()。注意一个容易被忽略的细节为什么已经调用tryAcquire失败后才进入acquire进入后还要再调一次tryAcquire因为从第一次失败到真正入队之间有时间窗口这期间锁可能已经被释放了所以acquireQueued前面还有一次机会直接获取成功省去入队开销。这是源码中典型的能省则省优化。3.2 addWaiter把当前线程包装成节点入队进入队列的代码是这样private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); Node pred tail; if (pred ! null) { node.prev pred; if (compareAndSetTail(pred, node)) { pred.next node; return node; } } enq(node); return node; }逻辑很清晰先尝试快速入队——把新节点的prev指向当前尾节点再用CAS把tail从旧的尾节点更新为新节点。CAS成功就说明入队成功把旧尾节点的next指向新节点即可。如果快速入队失败说明有多个线程同时在抢tail就退回enq(node)里用自旋CAS保证一定能入队。这里有个经典并发场景多个线程同时调用addWaiter它们都读到了同一个tail也都把自己的prev指向这个tail但CAS只有一个能成功。失败的线程会把tail重新读一遍再试这就是enq里的死循环逻辑。通过CAS自旋代替锁在高并发入队场景下既安全又高效。3.3 acquireQueued自旋、park与中断的博弈入队之后线程并没有直接陷入阻塞而是进入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; failed false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }这个循环是整个排队机制的核心。每个线程在队列里做两件检查当前节点的前驱是不是头节点如果是就再尝试一次tryAcquire如果前驱不是头节点或者抢锁失败就检查自己能不能安全挂起。为什么要求前驱必须是头节点才去抢锁因为头节点代表当前持有锁的线程或者已经释放锁的哨兵它释放锁后会唤醒后继节点。只有紧挨着头节点的线程才有资格第一时间接力抢锁其他节点就算抢到了锁也破坏了队形还会导致队列里的唤醒链断裂。这个约束是AQS保证公平排队的关键也是理解CLH队列性质的一个要点。shouldParkAfterFailedAcquire会检查前驱节点的waitStatus如果前驱是SIGNAL状态说明前驱承诺我释放时会唤醒你当前线程就可以放心地park如果前驱状态是0或PROPAGATE则通过CAS把前驱改成SIGNAL再循环回去试试抢锁如果前驱是CANCELLED就跳过它往前找一个有效前驱。这步操作用意是确保所有阻塞节点都有一个将来会被唤醒的保证避免线程永远睡死。最后parkAndCheckInterrupt才是真正调用LockSupport.park(this)让线程进入阻塞状态的行。线程被唤醒只有三种可能被前驱节点唤醒、被中断唤醒、或者被动地惊扰了一下。整个循环保证了一点只有真正抢到锁才会退出循环。3.4 公平与非公平tryAcquire的插队差异ReentrantLock默认是非公平锁它的tryAcquire实现长这样final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (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; }非公平的体现就在第一段只要当前state为0任何线程都可以直接CAS抢锁就算此刻队列里已经有线程在排队新来的线程依然可以插队。这是性能上的刻意取舍——插队成功的线程省掉了入队和挂起的开销但代价是队列中的线程可能被饿得更久。公平锁的tryAcquire则多了一个前置判断if (c 0) { if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) { ... } }hasQueuedPredecessors()用来检查队列中是否有比当前线程等得更久的节点如果有就不允许CAS抢锁老老实实去排队。这个检查看似简单实际上处理了并发下的边界情况如果tail不为空且头节点后继不是当前线程就得让位。所以公平锁的吞吐量通常低于非公平锁但每个等待者的最大等待时间有保障。我在实际项目里通常默认用非公平锁原因是绝大多数场景下短任务多非公平锁能避免频繁的线程挂起唤醒开销只有在追求严格公平或可能长耗时持锁的少数场景才切换到公平锁。性能测试也印证了这一点非公平锁在低竞争时吞吐能高出不少。4. 解锁之后release唤醒机制与同步队列的交接4.1 release的骨架一切以state为核心独占模式的解锁入口是release(int arg)public final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }和加锁对称tryRelease同样留给子类实现。以ReentrantLock为例protected final boolean tryRelease(int releases) { int c getState() - releases; if (Thread.currentThread() ! getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free false; if (c 0) { free true; setExclusiveOwnerThread(null); } setState(c); return free; }这里有一个非常重要的细节只有把state减到0才真正释放锁中间多次重入会一层层减下去直到归零才清空持有线程。所以重入几次就要释放几次只要有一次tryRelease没把state减到0其他线程永远拿不到锁。我曾经排查过一个线上问题原因就是有人在嵌套逻辑里少调了一次unlock导致state一直停在1所有线程卡了一整夜后来翻代码才发现是重入次数没配对。release里只唤醒头节点不唤醒其他排队节点因为CLH队列是接力式的唤醒头节点后继之后那个被唤醒的线程抢到锁轮到自己释放时再去唤醒它的后继。这个一次只唤醒一个的设计避免了惊群效应——所有线程同时醒来抢锁会白白消耗CPU。4.2 unparkSuccessor为什么唤醒的是后继节点的线程unparkSuccessor(Node node)是唤醒链上的核心方法private void unparkSuccessor(Node node) { int ws node.waitStatus; if (ws 0) compareAndSetWaitStatus(node, ws, 0); 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); }第一步先把头节点的waitStatus从SIGNAL清回0因为这代表我要唤醒别人这个承诺已经执行完了。然后找真正要唤醒的节点。正常情况是头节点的next但如果后继节点已经取消或者next为null就不能直接唤醒它了。此时源码不是从前往后找而是从tail往前遍历找到离头节点最近的一个有效节点。为什么从尾向前这是AQS设计里一个非常容易踩坑的细节。因为node.next这个引用可能因为并发入队而暂时为null——addWaiter先设置前置引用再CAS更新tail最后才设置旧尾节点的next中间窗口内从head往尾部看会看不到新节点。但prev引用是从头到尾依次建立的从tail往前找永远不会漏掉节点。这个反向遍历的思路理解了之后真的很佩服Doug Lea的严谨。4.3 从加锁到解锁走一遍一个线程的完整生命周期把加锁和解锁串起来看一个线程争抢锁的完整流程大致是这样第一步线程尝试CAS把state从0改成1成功则直接持锁执行任务全程无阻塞无排队。第二步如果CAS失败调用acquire(1)再次tryAcquire碰运气又失败则addWaiter把当前线程包装成节点挂到队尾。第三步进入acquireQueued循环判断前驱是否是头节点。如果不是经过shouldParkAfterFailedAcquire把前驱节点状态改成SIGNAL然后park挂起自己。第四步某个时刻持锁线程执行完调用unlock触发tryRelease把state减到0然后unparkSuccessor唤醒头节点的后继节点。第五步被唤醒的线程从parkAndCheckInterrupt返回再进入循环发现前驱恰好是头节点立刻tryAcquire抢锁成功后把自己设置为新头节点旧头节点的next被清空进入隔离区等待GC回收。这个生命周期里最让我感慨的是每个线程从入队到被唤醒中间经历的挂起和唤醒全都被精确控制在需要的时候而在高并发高峰期这种精确控制决定了整个系统的延迟曲线是否稳定。5. 不止是锁ReentrantLock、Semaphore、CountDownLatch如何复用AQS5.1 一图看懂AQS的共享/独占两条分支AQS对外提供了两种获取资源的模式。独占模式意味着同一时刻只有一个线程能拿到资源典型的ReentrantLock就是独占实现共享模式允许多个线程同时拿到资源只要资源还有余量典型的Semaphore和CountDownLatch都是共享实现。独占和共享的入口走的是不同的流程。独占走acquire/release共享走acquireShared/releaseShared。后者在获取资源时会循环尝试如果这次共享获取后还有剩余资源还会继续唤醒后继的共享节点形成传播唤醒效果。源码中的setHeadAndPropagate做的就是这件事private void setHeadAndPropagate(Node node, int propagate) { Node h head; setHead(node); if (propagate 0 || h null || h.waitStatus 0 || (h head) null || h.waitStatus 0) { Node s node.next; if (s null || s.isShared()) doReleaseShared(); } }这段代码的存在是为了让共享模式下多个等待线程能级联唤醒最高效地利用释放的资源。5.2 读锁写锁、信号量、门闩的实现差异并发工具使用模式state的语义关键钩子ReentrantLock独占重入计数tryAcquire / tryReleaseReentrantReadWriteLock独占共享高16位读锁计数低16位写锁计数tryAcquire / tryAcquireShared等Semaphore共享剩余许可证数量tryAcquireShared / tryReleaseSharedCountDownLatch共享剩余需要倒数的次数tryAcquireShared / tryReleaseSharedReentrantReadWriteLock的实现最精巧的一点是它只用了一个int字段同时表示读锁和写锁两个状态。高16位记录读锁被获取的总次数包括每个线程的重入次数之和低16位记录写锁重入次数。读锁用共享模式获取写锁用独占模式获取二者共用同一个state但各占一半比特位。Semaphore的tryAcquireShared核心是这样一个循环for (;;) { int available getState(); int remaining available - acquires; if (remaining 0 || compareAndSetState(available, remaining)) return remaining; }返回值大于0说明还有剩余许可证可以继续唤醒其他共享等待者返回负数说明已经没资源了线程入队等着。CountDownLatch则更简单直接初始化state为倒数次数每次countDown调用releaseShared(1)每次await调用acquireShared(1)。只有当state减到0时tryReleaseShared返回truedoReleaseShared会一次性唤醒所有等待线程。这也是CountDownLatch和Semaphore行为上最大的差别——前者是闸门一次全开后者是逐个放行。5.3 按需定制AQS留给扩展者的钩子想自己写一个AQS同步器只需要继承AQS类然后实现对应的try方法。我曾经给某模拟项目写过一个小型的一次性开关就是模仿CountDownLatch但允许重置状态class ResettableLatch extends AbstractQueuedSynchronizer { protected int tryAcquireShared(int arg) { return getState() 0 ? 1 : -1; } protected boolean tryReleaseShared(int arg) { return true; } }这个极简实现只有十几行就拥有了完整的等待-唤醒能力。AQS的框架把线程排队、阻塞、唤醒和取消这些最头疼的并发基础全部承担了开发者只需要专注于怎么定义state和什么时候比较状态。这也是AQS常年被称作并发基石的原因——把通用问题标准化把专属策略开放化。6. 条件队列ConditionObjectAQS的另一半6.1 为什么有了同步队列还需要条件队列很多人读AQS只盯着同步队列看但忘记了一个关键机制条件队列。Object.wait/notify在synchronized体系里用来实现条件等待而在AQS体系里这个能力由ConditionObject提供Condition接口的await和signal就是对应于wait和notify。条件队列存在的意义是让线程在持有锁的前提下主动放弃CPU和锁资源去等待某个业务条件成立。比如一个生产者线程发现缓冲区满了它不能继续往里面写又不能死循环最好的办法是释放锁然后睡在缓冲区不满这个条件上。等消费者取走数据后signal一下它再醒来抢锁继续工作。ConditionObject每个实例维护一条单向链表private transient Node firstWaiter; private transient Node lastWaiter;同一个AQS同步器可以有多个ConditionObject也就是说可以定义多组条件。这是比synchronized更灵活的地方——synchronized只有一个隐式条件队列而ReentrantLock.newCondition()可以创建多个条件分别管理不同业务状态的等待线程。6.2 await与signal的完整流程await()的核心流程非常惊艳public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node addConditionWaiter(); int savedState fullyRelease(node); int interruptMode 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode checkInterruptWhileWaiting(node)) ! 0) break; } if (acquireQueued(node, savedState) interruptMode ! THROW_IE) interruptMode REINTERRUPT; ... }关键步骤有三个。第一步addConditionWaiter把当前线程包装成条件队列节点注意此时节点状态是CONDITION挂到条件队列末尾。第二步fullyRelease会把当前线程持有的锁一次性全部释放——这意味着如果当前线程重入了5次这里会一次性释放5次把state归零。如果不全部释放其他线程永远拿不到锁而等待在条件上的线程也不可能有进展。第三步自旋等待直到节点被人从条件队列转移到同步队列才会尝试重新抢锁。signal()的入口则是doSignalprivate void doSignal(Node first) { do { if ( (firstWaiter first.nextWaiter) null) lastWaiter null; first.nextWaiter null; } while (!transferForSignal(first) (first firstWaiter) ! null); }transferForSignal是跨队列的关键操作。它先把条件队列节点的状态从CONDITION改为0然后调用enq把它插入同步队列队尾。如果这个入队前节点状态CAS失败说明节点已经被取消会再取下一个条件队列节点继续转移。这里面有个值得深思的细节signal只是把节点从条件队列搬到同步队列并没有直接唤醒它。真正让它醒过来要等它成为同步队列中的头节点后继被前驱节点调unparkSuccessor唤醒。也就是说signal之后线程不见得立刻执行它只是从睡在条件上变成排队抢锁什么时候执行为未知。这个signal后还要抢锁的语义和Object.notify其实是一致的只不过AQS把它组织得更清晰。6.3 一个跨队列的典型场景生产者消费者用Condition实现一个单生产者单消费者队列核心代码如下final Object[] items new Object[10]; int putptr, takeptr, count; final Lock lock new ReentrantLock(); final Condition notFull lock.newCondition(); final Condition notEmpty lock.newCondition(); public void put(Object x) throws InterruptedException { lock.lock(); try { while (count items.length) notFull.await(); items[putptr] x; if (putptr items.length) putptr 0; count; notEmpty.signal(); } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (count 0) notEmpty.await(); Object x items[takeptr]; if (takeptr items.length) takeptr 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } }两个条件队列在这里各司其职生产者睡在notFull上消费者睡在notEmpty上。消费者取走货物后notFull.signal()唤醒一个生产者生产者放入货物后notEmpty.signal()唤醒一个消费者。相比synchronized wait/notify用两个条件队列的好处是不会出现通知了同类线程的尴尬——notify无法指定唤醒哪个线程有时会唤醒不该醒的线程导致无意义的竞争而条件队列把不同类型的等待者隔离开了。我在读写源码时的一个强烈体会是条件队列和同步队列的组合就像一个酒店的等待大厅和候补名单——同步队列是已经获得资格但暂时拿不到资源的人排队条件队列是不满足条件所以主动退场的人在等待叫号。两者之间通过transferForSignal这个转乘通道衔接整个调度链路完整而紧密。7. 源码之外的实战认知重入、中断与排查思路7.1 可重入是怎么实现的从AQS源码反推可重入的实现非常清晰。nonfairTryAcquire里专门有一段else if (current getExclusiveOwnerThread()) { int nextc c acquires; ... setState(nextc); return true; }如果当前线程已经持有锁再次lock()时不需要CAS直接把state累加即可。tryRelease则是反向减去持有的次数。这个设计完全依赖AQS的state字段所以可重入的能力是AQS赋予所有子类的通用能力只要子类愿意在try方法里这样写。有一个隐蔽的坑值得提醒重入次数超过int上限时nextc 0会抛Error虽然日常业务几乎触发不到但长期运行的金融系统如果在一个事务里疯狂递归加锁还是有可能踩到。我个人习惯是写递归代码前先评估一下锁的重入深度避免这种极端情况。7.2 中断响应lock vs lockInterruptiblyReentrantLock默认的lock()对中断视而不见即使线程被interrupt()了依然会乖乖排队直到拿到锁只是最终会把中断标记恢复。lockInterruptibly()则不同它在等待过程中如果被中断会立刻抛出InterruptedException退出等待。这个差异在源码里的体现是lockInterruptibly走的是acquireInterruptibly它在parkAndCheckInterrupt发现中断后会直接抛异常而不是等拿到锁再自我中断。我在写网络请求框架的时候超时或者取消操作经常会用到lockInterruptibly因为取消任务时必须让线程迅速从锁等待中退出不能让它继续卡着。7.3 从AQS源码里悟出的排查并发问题的思路源码读多了之后最直接的收获不是背下那些方法名而是形成了一套排查并发问题的直觉。线程卡住不动第一反应是去看线程栈停在哪个方法如果是parkAndCheckInterrupt说明它正阻塞在AQS队列里原因要么是锁没有被释放要么是释放后唤醒链断了如果卡在await里则要去检查对应的条件是否永远不会满足。第二个直觉是关于锁的粒度。AQS的设计让我深刻感受到锁竞争的本质是state这个int变量的竞争。减少竞争的办法无非两种要么减少持锁时间要么减少锁的粒度。自己写完几个基于AQS的同步器后我对项目中那些粗粒度锁的代码会格外敏感因为它们可能正在造成大量线程排队。第三个直觉是提醒我重视线程取消清理。AQS里有cancelAcquire专门处理取消的节点会跳过取消节点维护队列完整性。工程中如果任由线程被取消而不做清理队列很快会被无效节点堵满导致真正的等待者迟迟得不到唤醒。这部分源码虽然不如加锁流程那么常被提到但真出线上事故时恰恰是这些边角逻辑在起作用。写了这么多最想告诉大家的是AQS不是什么玄学它就是一套极其务实的工程框架。把state的语义想清楚把同步队列和条件队列的两条唤醒链理顺再回头看ReentrantLock这些工具你会发现它们瞬间变得透明。源码读一遍不够建议至少自己动手写一个基于AQS的同步器哪怕只是模仿CountDownLatch写个一次性开关那种原来并发工具是这样搭建起来的体感比任何面试背诵都来得扎实。

相关新闻

一张图理清Python、IPython、Jupyter与Anaconda的关系与分工

一张图理清Python、IPython、Jupyter与Anaconda的关系与分工

刚开始学Python的时候,身边几乎每个人都在装Anaconda,打开别人给的Demo又要求用Jupyter跑,偶尔还会看到有人敲IPython……当时我整个人是懵的:我明明已经装了Python,为什么还要再装这一堆东西?它们是同一个…

2026/10/13 14:24:20 阅读更多 →
SPWM三相六路互补输出与调频调压:死区、载波比与V/F控制详解

SPWM三相六路互补输出与调频调压:死区、载波比与V/F控制详解

简介:面向STM32F103C8T6三相电机驱动与变频控制场景,该资源提供基于高级定时器TIM1查表法生成三相六路互补SPWM的完整工程。代码生成相位互差120的六路PWM,可配置死区时间,并通过恒压频比策略实现调频调压,适用于逆变器…

2026/10/13 14:23:19 阅读更多 →
西门子S7-1200 PLC机器人码垛与AGV调度系统实战解析

西门子S7-1200 PLC机器人码垛与AGV调度系统实战解析

先交代一下背景。这套项目是某食品企业码垛工段的自动化改造,原来的方案是人工作业加一台老旧机械臂,产能跟不上、垛形不稳定,客户一口气提了三个需求:自动码垛、空满垛转运、AGV自动接驳。设备清单下来的时候列的是两台西门子S7-…

2026/10/13 14:23:19 阅读更多 →

最新新闻

指标体系与数据采集:业务语言翻译与端到端契约实践

指标体系与数据采集:业务语言翻译与端到端契约实践

1. 这不是“搭个看板”那么简单:指标体系本质是业务语言的翻译工程很多人一听到“数据产品-指标体系与数据采集”,第一反应是:“哦,不就是埋点、接数、做BI看板?”——这种理解偏差,恰恰是多数数据项目半途…

2026/10/13 15:15:47 阅读更多 →
Postman+Jenkins在Windows上的接口自动化验证流水线

Postman+Jenkins在Windows上的接口自动化验证流水线

1. 项目概述:这不是“自动化测试”的幻觉,而是真实可落地的接口交付流水线起点你有没有过这样的经历:在Windows电脑上用Postman调通了一个API,截图发给同事说“这个接口好了”,结果第二天对方反馈“返回字段变了”“状…

2026/10/13 15:15:47 阅读更多 →
ABAP开发新范式:用XCO Libraries把数据字典操作变成积木式拼装

ABAP开发新范式:用XCO Libraries把数据字典操作变成积木式拼装

入行头几年,我一度觉得 ABAP 最不缺的就是"能干活但特别绕"的 API。建一张自定义表,要在 SE11 里一屏一屏地敲;程序里想看一下某个数据元素的属性,得先记住 DDIF_DTEL_GET 那套出参顺序。直到有一次做数据迁移准备&…

2026/10/13 15:15:47 阅读更多 →
轻量模型在算法复习中的“盲盒抽题”设计:动态调整题目难度与知识点

轻量模型在算法复习中的“盲盒抽题”设计:动态调整题目难度与知识点

很多刷过两三百道 LeetCode 的同学都会陷入同一种倦怠&#xff1a;按照“Hot 100”顺序从前往后刷&#xff0c;看到题目第一眼就能联想起官方题解里的关键变量名&#xff0c;甚至连双指针的 left < right 还是 left < right 都能肌肉记忆背出来&#xff1b;可一旦关掉题解…

2026/10/13 15:15:47 阅读更多 →
城市停车难诊断:多源异构数据清洗与时空特征工程实战

城市停车难诊断:多源异构数据清洗与时空特征工程实战

简介&#xff1a;本资源为2022年五一杯数学建模竞赛C题——火灾报警系统优化的完整建模解决方案&#xff0c;面向高校数学建模参赛学生、指导教师及应急管理领域研究者。针对探测器选型、误报研判、辖区管理评估与系统维护四大实际问题&#xff0c;资源构建了熵权-TOPSIS综合评…

2026/10/13 15:15:47 阅读更多 →
CTF逆向入门:从pyc文件到flag的Python字节码逆推实战

CTF逆向入门:从pyc文件到flag的Python字节码逆推实战

第一次看到 BUUCTF 里的 [GWCTF 2019] pyre&#xff0c;我以为是某个冷门语言框架的名字。点进去才发现&#xff0c;它给的是一个 .pyc 文件&#xff0c;考的是 Python 逆向。 pyre 这个题名放在 CTF 语境下&#xff0c;基本可以理解为 Python Reverse 的缩写&#xff0c;也…

2026/10/13 15:14:45 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目&#xff0c;我最大的感受是&#xff0c;大部分坑不是出在技术前沿&#xff0c;而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节&#xff0c;最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了&#xff0c;很难不碰到舵机控制这道坎。做智能小车要转方向&#xff0c;做机械臂要抬关节&#xff0c;做云台要偏航俯仰&#xff0c;背后都是同一个东西在干活&#xff1a;舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”&#xff1f;——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台&#xff1f;或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜&#xff1f;又或者&#xff0c;在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介&#xff1a;基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码&#xff0c;面向计算机相关专业课程设计与期末大作业学生&#xff0c;以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程&#xff0c;…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程&#xff1a;键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化&#xff0c;十个新手有八个栽在"往输入框里填东西"这件事上&#xff1a;要么填不进去&#xff0c;要么填了一半&#xff0c;要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程&#xff1a;阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀&#xff1a;什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面&#xff0c;跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →