简单纪要:浅谈ReentrantLock的父亲AbstractQueuedSynchronizer(AQS)的底层实现
在日常开发中,我们都直接或间接的使用过ReentrantLock,以及他的兄弟们Semaphore,CountdownLatch,ReadwriteLock,但是对于其底层是如果实现锁机制的呢?
下面我们先来看一张图:
大家可能在想,这张图里面并没有看到我们知道ReentrantLock,甚至连Lock我们也没有看到,不急,我可以先new一个Lock来看看:
Lock lock = new ReentrantLock();
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */ public ReentrantLock() { sync = new NonfairSync(); }
现在想必大家都明白了,我们在创建ReentrantLock的对象的时候 ,其实是创建了一个NonfairSync()对象,NonfairSync类是静态内部类,他继承了Sync,Sync继承了我们今天的主角AbstractQueuedSynchronizer;
现在我们先不急于看源码,我可以先看一张图,毕竟图是最直观的,走起,看图~
AQS是将每一条请求共享资源的线程封装为一个CLH锁队列的节点,AQS就是基于CLH实现的,内部维护了一个volatile int state 字段,线程通过CAS修改同步资源状态,成功则获取锁成功,失败则进入等待队列,等待唤醒;
volatile int state 字段可以通过三种方式操作:
- getState();
- setState();
- compareAndSetState();
AQS实现了两种资源共享的方式:独占(Exclusive)和共享(Share);
1. 从acquire(int arg)开始调取AQS中的程序,调取tryAcquire()方法获取同步资源(state)的状态,判断当前锁state>=0才可以获取到锁,获取一次,计数器进行 + 1 ,释放一次 -1 ;
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
2. 如果tryAcquire()获取同步资源state 小于0 ,返回失败;调取addWaiter(Node)方法,将当前线程放入等待队列的队尾,并返回当前队列;
将当前线程包装为Node,执行入队操作,将当前节点插入到等待队列的队尾;成功,返回当前线程包装为的node;
失败,调取enq(Node)方法;
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
3. 失败,调取enq(Node)方法,此方法 自旋 直到成功直接使用CAS操作 ,只能返回一个节点;
首先判断尾节点是否为空,就是判断队列是否初始化,因为 队列必须初始化, 当队列未初始化,创建一个空的标志结点作为head结点,并将tail也指向head;
如果队列初始化成功,使用CAS保证只有一个头节点初始化成功;
private Node enq(final 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; } } } }
4. addWaiter()方法执行完毕后,节点已经被加入到等待队列当中,当前线程处于挂起状态,等待被唤醒;调用acquireQueued()方法,返回获取资源的状态;
获取当前节点的前驱节点是 头节点,并尝试获取同步资源的状态成功,调用setHead()将当前节点设为头节点,setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了!
如果当前节点不是头节点,执行 shouldParkAfterFailedAcquire(p, node)方法,
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); } }
如果当前节点不是头节点,执行 shouldParkAfterFailedAcquire(p, node)方法处理获取锁失败的挂起逻辑,获取前驱的状态pred.waitStatus,如果已经设置如果前驱节点拿到资源后告诉的自己,即已经设置Node.SIGNAL,则可以进入等待状态,如果前驱节点已经取消,就跳过当前的前驱节点,到找到最近一个正常等待的状态,并排在它的后边,则之前的前驱节点无引用,则会被GC回收。
如果前驱节点状态正常,就设置前驱节点的状态为SINGAL,获取到资源后通知;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 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; 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; }
如果shouldParkAfterFailedAcquire()方法返回ture,证明前驱节点的状态已经设置好,调用parkAndCheckInterrupt()方法;
private final boolean parkAndCheckInterrupt() { LockSupport.park(this);// 调用park(),线程进入waiting状态 return Thread.interrupted();//返回线程中断状态 }
此时,AQS已经大体看完了,此时再看看我们开篇提供的流程图,想必大家理解的会更加深刻,希望能对大家有所帮助,有问题请指出哦^_^~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律