AQS公平Fair与非公平UnFair同步____ReentrantLock排他模式以及CountDownLatch共享模式下的同步过程。

1 篇文章 0 订阅

前言

本文直接进行源码解释,源码中运用了大量的短路判断逻辑,以下是AQS的几种步骤:

六个操作:

  1. 抢锁
  2. 释放锁
  3. 入队
  4. 出队
  5. 阻塞
  6. 唤醒

 先列出队列节点的各个字段:

CANCELLED:在拿到锁(acquire)之前就取消了。

SIGNAL:下一个节点正在等待锁。

CONDITION:当前节点是条件队列的节点。

 

 waitStatus字段初始化为0,在条件队列中初始化为CONDITION,要用CAS来原子性的改动。

 此外还有前驱指针和后继指针来维护双向队列,以及表明当前线程的字段。

 

nextWaiter字段 在条件队列中,指向下一个等待的节点,在非条件队列中会为SHARED值。

 


ReentrantLock下的过程:

以及下面它的公平实现和不公平实现。

现在我们从reentractlock的lock方法来出发,当调用lock方法时,调用的是Sync的lock方法,但是这个方法是抽象的,由它的子类NonfairSync和FairSync来实现,默认情况下是非公平的,当传参为true时,调用带参的公平构造。

 

来看看非公平的lock方法:

先通过一个CAS操作去尝试将state从0变成1,也就是去尝试获取锁,修改成功后,将当前持有锁的线程信息中加入自己的信息。

如果修改失败,证明现在正在有线程持有锁,就会进入下面的acquire方法,这就进入了AQS里面:

 在进入方法后,会先去尝试获取锁,执行tryAcquire方法,那么我们先来看看非公平的tryAcquire: 

 

可以看到,在获取到线程和state信息后,判断state是否为0(也就是现在没有线程拥有锁),会让线程去尝试CAS操作获取锁,如果获取成功后,就会设置当前线程信息为本线程,否则将会到else if判断此线程是否为已经获取到锁的线程,如果是的话,那就是重入操作了,重入时,state状态会加一然后再去setState修改state值。

如果都不满足上述条件,也就是说没有抢到锁也不是重入线程,就会return false:

回到这里,当返回false时,不短路,然后就要进行addWaiter入队列的操作了:

 

这里,进入后,创建线程所对应的节点(封装),两个参数,其一是当前线程,其二是模式,这里用的就是EXCLUSIVE排他(独占)模式,

 

 

 

如果队列没有被初始化,进入上图的enq方法,进行初始化(第一个加入队列的负责初始化,创建头结点和自身节点),后续再来的线程会进行入队列操作,上面的if(pred != null)的判断也是入队列操作,enq也是入队列的操作,只不过上述效率较快,不用再去进入enq判断,直接改指针即可。

我们也可以发现,enq执行完后返回当前节点的前驱节点(node.prve = t),addWaiter返回的是当前节点。

在此之后,就要针对当前节点去执行acquireQueued方法来尝试入队列。

 

进入方法后,取当前节点的前驱节点,这里又用到了短路判断,判断如果是头结点的话,就会去tryAcquire,再次尝试去获取锁,(这里也就是说,我构建了队列后可以再去看看是不是可以拿到锁了,因为在此期间其他线程可能已经释放了锁),如果没有拿到锁,就会进入shouldParkAfterFailedAcquire方法:

 

首先会去拿到前驱节点的waitStatus等待状态,判断如果waitStatus是-1时,返回true,返回后,这里又用到了短路,会去执行parkAndInterrupt方法来挂起当前线程,线程被唤醒时,会继续向下执行,也就是继续循环判断自己为头结点的话就去征用锁。

但是waitstatus起始值都是0,一直往下判断,到else块里,会强制的CAS把watistatus设置成SIGNAL,也就是-1.

也就是说,当你初始化队列阶段,会有两次额外争取锁的机会。

unlock:

调用unlock方法时,会调用sync的release方法:

 进入release方法:

 

 它会先调用tryRelease方法:

先拿到state并且减去release(1),然后判断当前线程和持有锁的线程是否为同一个线程,不是则抛异常,线程匹配上之后,判断当c为0后,会释放锁,并且把当前持有锁的线程置为null。

然后返回free字段(如果是重入,c不为0,free字段就不为true,只会将重入等级降低)。

再回到我们的release方法中:

如果释放锁成功,就会进入if语句,首先会拿到头结点,内部判断头结点是否存在,并且头结点的waitStatus不为零的话(代表头节点后有节点在等待),就会执行unparkSuccessor方法,将要唤醒的节点的前驱节点作为参数传入(基本上都是将头节点传入,因为只有头节点之后的节点才能去申请锁)。

unparkSuccessor方法: 

 

首先判断节点的waitstatus是不是小于零(-1),小于则设置成0,然后拿到next节点(也就是在等待的节点),将其取出,执行unpark方法,进行唤醒。

我们之前讲到,第一个节点被阻塞到了队列里,此时被唤醒后,将会去执行抢锁,抢锁成功就去执行,失败就继续park阻塞。

 

 

这里做一个小结:

当A线程正在持有锁,B线程过来,尝试获取锁失败,就会尝试去初始化一个队列,此时会有一次再申请锁的机会,如果还是失败,则去初始化队列,将自己封装成队列中的节点,在入队列时生成两个节点,一个头结点和自身节点,因为在初始化队列的过程中很有可能A已经释放锁了,所以B还有一次获取锁的机会,此时如果获取失败,就会park阻塞,当A进行release操作释放锁时,会去unpark来唤醒头结点的后继结点,也就是B节点,此时,B会去抢锁,但是,如果在此时有另一个C线程刚到这里,没有进队,他就会去抢锁,C抢锁成功,则B又park阻塞,C抢锁失败,会入队然后阻塞,B抢锁成功的话,就会出队并且头结点后指。

Fair公平锁下:

在tryAcquire方法下比非公平锁多了一个判断,判断前面是否在头部,返回true就代表不在头部,前面有等待节点,由此来保证公平。

总结:公平锁和非公平锁只有两处不同:

  1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
  2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。


CountDownLatch下AQS:

 先来看看最重要的两个方法:

await阻塞,countDown计数器减一。

CountDownLatch是共享模式(node节点为SHARED,ReentrantLock的是EXECLUSIVE)。

现从await方法出发:

 进入其中的acquireSharedInterruptibly方法,

 

这个方法是支持中断的,会对其他线程发出的中断做出响应。

总结:

CountDownLatch 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。

CountDownLatch 是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行。

 


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值