“奇米第四声7777777”——解码正品密码,拒绝伪劣陷阱

核心内容摘要

嘘!别告诉妈妈:那些只属于我们的秘密花园
舌尖上的秘密:吃瓜爆料最佳路线,让你成为信息时代的“内行”!

极尽感官诱惑:探索“爱液”视频网站官方版的无尽魅力与视听盛宴

先搞懂核心基础ReentrantLock和AQS的关系你可以把AQSAbstractQueuedSynchronizer理解成一个“同步组件的通用骨架”——它提前写好了同步队列管理、线程阻塞/唤醒的核心逻辑只留了几个“钩子方法”比如tryAcquire、tryRelease让子类自己实现。

钩子方法的通俗理解钩子方法的核心可以用一个生活化的比喻你去餐厅吃“套餐”餐厅父类已经定好了套餐的固定流程先上开胃菜 → 上主菜 → 上甜品。

但“主菜选什么”比如选牛排还是烤鱼这个步骤餐厅留了一个“钩子”让你自己选——流程不变但关键细节由你定制。

对应到编程中钩子方法是模板方法模式的核心父类通常是抽象类定义了一个算法的整体骨架模板方法其中把算法中“需要子类定制的步骤”设计成空方法/抽象方法这就是钩子方法子类通过重写这些钩子方法就能在不改变整体流程的前提下定制算法的具体细节。

钩子方法的正式定义钩子方法Hook Method由父类声明可以是抽象方法、空实现方法甚至带默认实现的方法父类的“模板方法”会调用这些钩子方法但不关心钩子方法的具体实现子类通过重写钩子方法实现自己的业务逻辑从而“钩入”父类的固定流程中。

结合AQS的例子你最熟悉的场景这是理解钩子方法最好的例子因为AQS就是模板方法模式的经典应用

AQS的“模板方法”固定流程AQS定义了加锁/解锁的整体流程模板方法比如加锁流程acquire(int arg)→ 先调用tryAcquire→ 失败则入队 → 自旋/阻塞解锁流程release(int arg)→ 先调用tryRelease→ 成功则唤醒队列线程。

这些acquire、release是AQS的模板方法流程完全固定子类不能改。

AQS的“钩子方法”子类定制AQS把“抢锁/释放锁的具体逻辑”设计成钩子方法留给子类比如ReentrantLock的FairSync/NonfairSync实现AQS的钩子方法作用AQS的默认实现子类ReentrantLock的定制实现tryAcquire(int)尝试获取独占锁直接抛UnsupportedOperationException公平锁排队抢锁非公平锁允许插队抢锁tryRelease(int)尝试释放独占锁直接抛异常减少state重入次数清空锁持有者isHeldExclusively()判断当前线程是否持有独占锁返回false判断exclusiveOwnerThread是否为当前线程

具体例子tryAcquireAQS中tryAcquire的默认实现钩子方法// AQS中的钩子方法抽象模板里的“空钩子”protectedbooleantryAcquire(intarg){thrownewUnsupportedOperationException();}ReentrantLock的NonfairSync重写这个钩子方法定制抢锁逻辑// 非公平锁定制的钩子方法实现protectedfinalbooleantryAcquire(intacquires){returnnonfairTryAcquire(acquires);// 非公平抢锁逻辑} 核心逻辑AQS的acquire流程不变但tryAcquire的具体逻辑由ReentrantLock的子类决定——这就是钩子方法的

核心价值。

钩子方法的核心特点父类控流程子类填细节父类的模板方法定死了“做什么步骤”子类的钩子方法决定“步骤怎么做”灵活性高同一套父类流程不同子类重写不同钩子方法就能实现不同功能比如AQS既可以实现ReentrantLock也能实现CountDownLatch访问修饰符钩子方法通常是protected便于子类重写模板方法通常是public final防止子类修改流程。

钩子方法 vs 普通重写方法很多新手会混淆核心区别维度钩子方法普通重写方法调用方父类的模板方法主动调用通常由外部直接调用作用定制父类流程中的某个步骤完全替换父类方法的逻辑与父类的耦合度强耦合依赖父类的流程弱耦合仅继承关系

总结钩子方法的核心父类定“流程骨架”子类通过重写钩子方法定制“流程中的细节”是模板方法模式的核心AQS中的典型应用tryAcquire/tryRelease是钩子方法acquire/release是模板方法ReentrantLock通过重写钩子方法实现公平/非公平锁本质价值在不改变整体流程的前提下让子类灵活定制核心逻辑兼顾流程统一和细节灵活。

结合你之前看的ReentrantLock源码钩子方法就是AQS留给FairSync/NonfairSync“定制抢锁逻辑”的入口这也是为什么ReentrantLock能同时支持公平和非公平锁——只需要重写tryAcquire这个钩子方法即可。

而ReentrantLock重入锁就是基于这个骨架做的“定制化产品”ReentrantLock内部有两个核心子类FairSync公平锁、NonfairSync非公平锁它们都继承自AQS你创建ReentrantLock时传true就是公平锁传false就是非公平锁本质是初始化不同的sync对象FairSync/NonfairSyncReentrantLock是“独占锁”——同一时间只有一个线程能持有锁。

AQS的核心设计支撑ReentrantLock的底层结构AQS的核心是“同步队列状态管理”先把这两个基础搞懂才能理解加锁解锁的源码。

同步队列线程排队的“双向链表”当线程抢锁失败时会被封装成一个“结点Node”加入队列等待这个队列是带头尾指针的双向链表head队列头结点代表当前正在持有锁的线程tail队列尾结点新抢锁失败的线程会加到尾部队列遵循FIFO先进先出——正常情况下先排队的线程先抢锁。

Node结点排队线程的“身份证”每个抢锁失败的线程都会被包装成一个Node对象里面存了关键信息属性作用prev指向前一个结点双向链表的“前向指针”next指向后一个结点双向链表的“后向指针”thread当前结点对应的线程比如线程A抢锁失败这里就存AwaitStatus结点状态ReentrantLock中主要用3个值0初始化状态-1SIGNAL当前结点的前驱释放锁后要唤醒当前结点的线程1CANCELLED线程等待超时/被中断放弃抢锁

state锁的“重入次数计数器”AQS里有个volatile int state加了volatile保证可见性在ReentrantLock里它的含义是“锁被线程持有的重入次数”state0锁是空闲的没有线程持有state1锁被某个线程持有未重入state1锁被同一个线程多次重入比如线程A拿到锁后又调用了一次lock()state就变成2。

exclusiveOwnerThread锁的“持有者标记”这个属性存在AQS的父类里专门用来标记“当前哪个线程持有独占锁”——比如线程A拿到锁这个属性就存AA释放锁后这个属性清空。

非公平锁的加锁流程重点实际开发用得最多我们以你笔记里的测试代码为入口一步步拆解lock.lock()的底层逻辑ReentrantLocklocknewReentrantLock(false);// 非公平锁lock.lock();// 核心加锁方法步骤1NonfairSync的lock()方法——第一次“插队”抢锁非公平锁的lock()方法会先做一个“快速抢锁”// NonfairSync的lock()核心逻辑finalvoidlock(){// 第一步CAS尝试把state从0改成1只有锁空闲时能成功if(compareAndSetState(0,

)// 抢锁成功标记当前线程为锁的持有者setExclusiveOwnerThread(Thread.currentThread());else// 抢锁失败走AQS的acquire方法acquire(

;} 这里的“非公平”体现哪怕队列里有线程在排队新线程也会先试试能不能直接抢锁不按排队顺序。

步骤2acquire(

——抢锁失败后的“正式流程”acquire是AQS的模板方法核心逻辑如下伪代码publicfinalvoidacquire(intarg){// 三个核心方法tryAcquire再抢一次锁、addWaiter加队列、acquireQueued阻塞等待if(!tryAcquire(arg)addWaiter(Node.EXCLUSIVE)!nullacquireQueued(addWaiter(Node.EXCLUSIVE),arg)){// 处理中断非核心暂时忽略}}我们拆解这三个核心方法子步骤

1tryAcquire(

——第二次“插队”抢锁tryAcquire是AQS的钩子方法NonfairSync重写了它底层调用nonfairTryAcquire(

这是“通用抢锁逻辑”覆盖所有场景finalbooleannonfairTryAcquire(intacquires){finalThreadcurrentThread.currentThread();intcgetState();// 获取当前锁的状态// 场景1锁空闲state0if(c

{// 再CAS抢一次锁第二次插队机会if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;// 抢锁成功}}// 场景2锁已经被当前线程持有重入elseif(currentgetExclusiveOwnerThread()){intnextccacquires;// 重入次数1比如state从1变2setState(nextc);returntrue;// 重入成功}// 场景3锁被其他线程持有returnfalse;// 抢锁失败} 为什么lock()里先做一次CAS又在tryAcquire里做一次—— 为了性能对“锁空闲”这个高频场景做提前处理减少后续复杂逻辑的调用。

子步骤

2addWaiter(Node.EXCLUSIVE)——抢锁失败线程入队如果tryAcquire失败线程会被封装成Node结点加入同步队列尾部。

这个方法的核心是enq(node)保证线程安全入队privateNodeaddWaiter(Nodemode){//

创建新Node封装当前线程独占模式NodenodenewNode(Thread.currentThread(),mode);//

先尝试快速入队性能优化Nodepredtail;if(pred!null){node.prevpred;if(compareAndSetTail(pred,node)){pred.nextnode;returnnode;}}//

快速入队失败比如队列为空/有竞争走enq方法enq(node);returnnode;}// 核心无锁化保证线程安全入队CAS循环privateNodeenq(finalNodenode){for(;;){// 死循环直到入队成功Nodettail;// 场景1队列为空head/tail都是nullif(tnull){// CAS创建空的头结点这个结点不存线程只是“占位符”if(compareAndSetHead(newNode()))tailhead;// 尾指针也指向头结点}// 场景2队列非空else{node.prevt;// CAS把尾指针改成当前结点if(compareAndSetTail(t,node)){t.nextnode;// 原尾结点的next指向当前结点returnt;}}}} 关键理解队列为空时先创建一个“空的头结点”没有线程再把当前线程的Node加到尾部用“CAS死循环”实现无锁化入队——不用锁也能保证多线程下结点安全加到尾部快速入队是性能优化避免每次都走循环。

子步骤

3acquireQueued(Node, arg)——入队后线程阻塞等待线程入队后不会立刻阻塞而是进入“自旋抢锁阻塞”的循环finalbooleanacquireQueued(finalNodenode,intarg){booleanfailedtrue;try{booleaninterruptedfalse;for(;;){// 死循环直到抢锁成功finalNodepnode.predecessor();// 获取前驱结点// 场景1前驱是头结点说明轮到自己抢锁了if(pheadtryAcquire(arg)){setHead(node);// 把当前结点设为新的头结点p.nextnull;// 原头结点出队垃圾回收failedfalse;returninterrupted;// 抢锁成功退出循环}// 场景2前驱不是头结点或抢锁失败——准备阻塞if(shouldParkAfterFailedAcquire(p,node)parkAndCheckInterrupt()){interruptedtrue;}}}finally{if(failed)cancelAcquire(node);}}这里又拆两个核心方法① shouldParkAfterFailedAcquire确保自己能被唤醒线程阻塞前必须确保“前驱结点会唤醒自己”否则阻塞后永远醒不过来privatestaticbooleanshouldParkAfterFailedAcquire(Nodepred,Nodenode){intwspred.waitStatus;// 获取前驱结点的状态// 场景1前驱状态是SIGNAL-1——前驱释放锁后会唤醒当前线程if(wsNode.SIGNAL)returntrue;// 可以放心阻塞// 场景2前驱状态是CANCELLED1——前驱放弃抢锁了if(ws

{// 往前找直到找到一个状态不是CANCELLED的结点do{node.prevpredpred.prev;}while(pred.waitStatus

;pred.nextnode;// 把当前结点挂到正常结点后面}// 场景3前驱状态是0初始化else{// CAS把前驱状态改成SIGNAL-1compareAndSetWaitStatus(pred,ws,Node.SIGNAL);}returnfalse;// 暂时不阻塞继续循环} 核心目的确保当前结点的前驱状态是SIGNAL这样前驱释放锁时会唤醒自己。

② parkAndCheckInterrupt阻塞当前线程如果shouldParkAfterFailedAcquire返回true就调用这个方法阻塞线程privatefinalbooleanparkAndCheckInterrupt(){LockSupport.park(this);// 阻塞当前线程线程进入WAITING状态returnThread.interrupted();// 检查线程是否被中断重置中断标记}LockSupport.park()是JDK底层的阻塞方法比Object.wait()更灵活不用依赖synchronized。

非公平锁加锁流程

总结一句话线程先两次“插队”抢锁lock()的快速CAS tryAcquire的通用CAS都失败后封装成Node入队入队后自旋检查是否轮到自己抢锁没轮到就确保前驱会唤醒自己然后阻塞被唤醒后继续自旋抢锁直到成功。

解锁流程lock.unlock()解锁比加锁简单核心是“减少重入次数 唤醒队列里的线程”。

步骤1unlock()调用release(

publicvoidunlock(){sync.release(

;// 调用AQS的release方法}publicfinalbooleanrelease(intarg){// 第一步尝试释放锁if(tryRelease(arg)){Nodehhead;// 第二步队列非空且头结点状态不是0说明有线程在等待if(h!nullh.waitStatus!

unparkSuccessor(h);// 唤醒后继线程returntrue;}returnfalse;}步骤2tryRelease(

——释放锁减少重入次数tryRelease是AQS的钩子方法ReentrantLock重写了它protectedfinalbooleantryRelease(intreleases){intcgetState()-releases;// 重入次数-1比如state从2变1// 校验只有持有锁的线程能释放锁if(Thread.currentThread()!getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfreefalse;// 场景1重入次数减到0完全释放锁if(c

{freetrue;setExclusiveOwnerThread(null);// 清空锁的持有者}// 场景2重入次数还没到0只是减少一次重入setState(c);returnfree;// 只有完全释放锁才返回true} 为什么不用CAS—— 因为只有持有锁的线程能调用unlock()是单线程操作不需要同步。

步骤3unparkSuccessor(h)——唤醒队列里的线程如果完全释放了锁就唤醒队列里的下一个线程privatevoidunparkSuccessor(Nodenode){intwsnode.waitStatus;// 把头结点状态重置为0if(ws

compareAndSetWaitStatus(node,ws,

;Nodesnode.next;// 头结点的后继结点// 场景1后继结点被取消状态1或为空if(snull||s.waitStatus

{snull;// 从队列尾部往前找找到离头结点最近的正常结点for(Nodettail;t!nullt!node;tt.prev)if(t.waitStatus

st;}// 场景2找到正常的后继结点唤醒它if(s!null)LockSupport.unpark(s.thread);// 唤醒线程} 为什么从尾部往前找—— 因为结点的next指针可能被修改比如线程取消等待而prev指针更稳定能确保找到正确的线程。

非公平锁的“不公平”本质 为什么用它

不公平的原因同步队列是FIFO先进先出正常情况下先排队的线程该先拿锁但非公平锁允许“插队”抢锁阶段新线程在入队前有两次抢锁机会哪怕队列里有线程在等释放阶段线程A释放锁时先把state改成0锁空闲再去唤醒队列里的线程C这中间的“时间差”里新线程B可能直接抢锁成功导致队列里的C继续等待。

极端情况队列里的线程可能一直抢不到锁饥饿问题。

为什么实际开发多用非公平锁因为性能更高减少线程切换如果新线程能直接抢到锁不用入队、阻塞、唤醒避免了线程上下文切换的开销高并发下优势明显虽然有饥饿风险但实际业务中锁持有时间短饥饿概率极低性能收益远大于风险。

公平锁的核心差异补充公平锁和非公平锁的核心区别只有两点公平锁的lock()方法没有“快速CAS抢锁”这一步直接走acquire(

公平锁的tryAcquire方法里除了判断state还会检查“队列里有没有线程在等”——如果有哪怕state0也不会抢锁直接排队。

公平锁严格按FIFO顺序抢锁没有插队但性能低线程频繁阻塞/唤醒。

总结核心要点回顾ReentrantLock的底层是AQSAQS提供同步队列双向链表、state重入次数、线程阻塞/唤醒能力ReentrantLock通过FairSync/NonfairSync重写AQS的钩子方法实现公平/非公平锁非公平锁加锁流程两次插队抢锁lock()快速CAS tryAcquire通用CAS→ 抢失败则入队 → 自旋检查前驱是否为头结点 → 确保能被唤醒后阻塞 → 被唤醒后继续自旋抢锁解锁流程减少state重入次数 → 完全释放锁后state0→ 从队列尾部往前找正常结点并唤醒非公平锁的取舍牺牲“公平性”可能饥饿换取高性能这是实际开发中选择它的核心原因同步队列的核心设计用CAS循环实现无锁化入队Node的SIGNAL状态保证线程能被唤醒FIFO特性是公平锁的基础。

别告诉妈妈-别告诉妈妈应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123