AQS 获取锁源码解析
没图片,我从不BB
1.锁最频繁的2大操作之一:获取 acquire - (以独占式锁为例)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
结合上面的故事说明:
- 用户调用lock,执行获取锁方法(顾客到按摩店前台,招呼老板娘安排技师)
- 执行内部方法 acquire --> 内部调模版方法 tryAcquire (老板娘查看技师是否作业中,是否有顾客在服务):
- tryAcquire返回true,获取到锁:end (情况1:技师空闲,无顾客在服务-->登记顾客,技师工作中)
- 在锁被占用的时 ( 情况2:技师工作中,有顾客服务中--->提醒顾客取号):
- 先执行addWaiter,将当前Node到队尾(顾客先领取老板娘的号码牌,进入排队中)
- 然后执行AcquireQueued方法,该方法目的有二:( 排队顾客不时问老板娘:下一个是轮到我吗?技师有空了没?)
- 拦截所有不是Head-Next的节点,保证在队列中的线程获取锁顺序的公平性(老板娘回答:下一个轮到1号,给我看下你的号码牌: --->1.是1号哟,里面请。 2.不是,继续等着吧)
- 所有等待队列的线程进入到Park状态,状态为calceled=1的线程会从等待队列中剔除(老板娘每次也会你前一个人是否走,走的号码牌就注销)
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;
}
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 和 parkAndCheckInterrupt 这2个东西要放到一起来看
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//从这里我们就知道: NodeA.status=SIGNAL表示 下一个 NodeA.next 是要被 PARK 的
return true; //true : 对就是你赶紧下车!!!
if (ws > 0) {
//就从不断循环,不断将 node.prev指向更前面没有Canceled的node,我们知道,这个地方是为
//了删除掉那些已经canceled的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//能走到这 ,pred =0/ <-1 ,要是等于0 说明这是一个新加入的节点,那在给一次tryAcquire
//的机会, 要是<-1 那说明这个节点是 await进来 or 这是个共享传播锁,那必须在给一
//次 tryAcquire 机会
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; //false : 表示本次不Park
}
No Picture no BB
不是说这个解释错,大伙时间这么宝贵,不想浪费大家时间
通过一个真实的的案例来带我们理解这个acquire的工作:
实例化一个ReentrantLock的锁之后,开启10个线程,每个线程加一把锁并且执行的工作是休眠2分钟,这里还是要强调一下,同一把锁每次只能有一个线程占有,开10个线程,每60s开启一个,第一次启动,线程直接通过tryAcquire()获取到锁,执行休眠 ,再然后其他的线程会每隔60s进来,此时线程会尝试去拿锁,失败之后会进入到等待队列中,等待被唤醒!上一步最后面说的尝试去拿锁是 通过tryAcquire方法,这个方法是一个在继承AQS抽象类之后自己主动去实现的类。在尝试拿锁失败之后就会去执行 acquireQueued(addWaiter(null),arg)执行 acquireQueued(addWaiter(null),arg) 先要去执行addWaiter(null) 表示在线程获取锁失败之后要将Thread 包装成Node,然后存放到 等待队列的尾巴上,(不用担心等待队列是否为空,这个队列是个双向队列,保证了获取锁的顺序也就是公平性) 此时nodeA就会进入到acquireQueued() 方法中此时acquireQueued 拿到了已经放到尾巴上的nodeA 然后进行自旋{ 先去把这个nodeA 的前驱节点nodePre拿到 ,看一下前驱节点是头节点吗,是 -- 那就再次尝试获取锁。【拿到了那就释放掉这个nodeA,over /没拿到那就继续往下看】 ,不是那就继续执行下面的判断 shouldParkAfterFailedAcquire(nodePre,nodeA), && ParkAndCheckInterrupt() .进入到shouldParkAfterFailedAcquire(nodePre,nodeA) 方法,此时nodePre是head ,waitStatus int 类型 默认值是0。0的情况会进入到if的第三个判断,里面的操作是将nodePre的waitStatus 设置成 -1,返回false,这样在acquireQueued 的自旋进行第二次的时候,判断nodePre的waitStatus = -1,返回true,会调用 ParkAndCheckInterrupt() , 可以防止一直运行自旋,消耗资源。此时就算是已经将任务提交到等待队列中了。要是这个时候又有新的线程进入, 就是去重复 2~5 的动作。 一个Lock使用完之后会调用unlock方法,这就是将等待队列中的Park状态给打断 ,此时队列中Head -Next ->nodeB 就可能tryAcquire 成功,然后nodeB-Next->nodeC ,nodeC 就成为Head-Next->nodeC ,就可以在下一次锁竞争中 tryAcquire 了。 基本上就是这个样子了
主要的东西都已经说明了,在理解的过程中碰到的几个问题
- 新Node的waitStatus未设置值吗?是的(老板娘一开始肯定笑脸相迎,允许你问2次 "是否叫d到号了")
- AcquireQueue 中有for(;;) , 不断运行,会不会占用很高资源?不用担心,里面有叫停功能,LockSupport.park(老板娘会惯着你一直问有没有姑娘空吗???)
2.讲了acquire 那可以顺提一下release 方法
在理解了acquire的实现方式之后,在理解release 就非常简单了:
在调用unLock之后当前占用的线程表示占用结束,下一个线程可以去竞争锁了,在公平锁的情况下,锁的释放过程 就是一个将等待队列的Head-Next的nodeA进行 unPark,waitStatus=0(unparkSuccessor),并且将Head-Next 指向 nodeA-Next = nodeB ,这个时候 要是nodeA 执行tryAcquire成功,就会执行nodeA的线程,然后修改nodeA ---> prev=null,thread=null (acquireQueued) ,简单吧,共享式的有细微的差别,唤醒动作是共享传播式