公平锁非公平锁的实际使用_由ReentrantLock的非公平锁简画流程和源码分析了解AQS...

AQS:AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架
官方文档:提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量,事件等)。该类被设计为大多数类型的同步器的有用依据,这些同步器依赖于单个原子int值来表示状态。主要是通过维护一个state 变量以及用来存放阻塞线程的双向队列来控制同步

state 0代表释放,1或者>1(锁重入)代表被占有,

从ReentrantLock的非公平锁的lock实现来看AQS:

一开始没想写源码分析,总感觉少点什么,补上之后,结合流程图和AQS队列流程还是比较容易理解的,请耐心看源码分析的注释

40a8910314b814e22da9bf040b83c66a.png

对着源码看更容易理解,难于理解的在于对AQS双向队列的操作:

AQS双向队列操作:假设ABC三个线程,A先获取到锁

be05b6e5acf2623c7410225aafebf815.png

锁释放:

dfffb1e990202932261503d1862b6092.png

ReentrantLock源码分析:

//非公平锁NonfairSync

static final class NonfairSync extends Sync {

private static final long serialVersionUID = 7316153563782823691L;

/**

* Performs lock. Try immediate barge, backing up to normal

* acquire on failure.

*/

//lock入口

final void lock() {

//CAS 直接去获取锁,尝试修改state 0变为1。修改成功则获取锁成功。

//这里用CAS 修改成功返回true,修改失败说明state已经不是0,被其他线程抢先修改了,返

//回false

if (compareAndSetState(0, 1))

//成功获取锁,设置持有锁的线程为自己也就是当前线程

setExclusiveOwnerThread(Thread.currentThread());

else

//CAS获取所失败,再次尝试获取锁,如果锁已经释放,这步就会有很大的性能提高

//这里是父类的抽象,子类实现就是下面的tryAcquire

acquire(1);

}

//非公平锁尝试获取锁

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

}

下面看CAS获取锁失败的逻辑,进入acauire(1): 

//AQS尝试获取锁

public final void acquire(int arg) {

//tryAcquire需看子类的具体实现,这里看的是NonfairSync的tryAcquire

//实现nonfairTryAcquire

if (!tryAcquire(arg) &&

//尝试获取所失败,会为当前线程建立线程节点放入队列尾走addWaiter(Node.EXCLUSIVE)

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

//中断补偿

selfInterrupt();

}

//NonfairSync的tryAcquire实现

//尝试获取锁,true获取锁成功,false失败

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

//state=0可以进行获取锁

if (c == 0) {

if (compareAndSetState(0, acquires)) {

//CAS成功获取锁,设置持有锁的线程为当前线程

setExclusiveOwnerThread(current);

return true;

}

}

//c!=0说明锁已经被持有,判断是已经持有锁的线程,(可重入)

else if (current == getExclusiveOwnerThread()) {

//可重入锁 state+1

int nextc = c + acquires;

//加1之后小于0,state值不对抛出异常

if (nextc < 0) // overflow

throw new Error("Maximum lock count exceeded");

//可重入锁加1后赋值

setState(nextc);

return true;

}

return false;

}

//新建线程节点,mode以给定模式构造结点,有两种:EXCLUSIVE(独占)和SHARED(共享)这里是独占

//ReentrantLock为独占锁

private Node addWaiter(Node mode) {

//为当前线程建立节点,独占模式,当前线程也就是刚刚来尝试获取锁失败的线程

Node node = new Node(Thread.currentThread(), mode);

// Try the fast path of enq; backup to full enq on failure

//这里想把新建的节点放入队尾,所以当前队尾的next 应指向新建的节点

Node pred = tail;

//当前队尾不为空,就是说当前队列里有没有其他线程节点在等待

if (pred != null) {

//新建节点的前置指向当前队尾节点,

//这里就像流程图里,放入C线程节点的时候,C的前置指向B

node.prev = pred;

//然后CAS更新尾巴节点为新建节点

if (compareAndSetTail(pred, node)) {

//更新成功,把队列的当前队尾节点的下一个节点指向新建节点

//因为是双向队列,要改变或确定一个节点的位置 就要指定节点的prev和next

//就是我在你后面,我的前面是你,你的候面是我,才能确定我在你后面

//这里有个问题,因为分三步操作:1 修改新建节点的前置。2 CAS修改尾巴节点tail

//3 修改新建节点的前置节点的后置,三步操作不是原子操作,思考点,有可能我

//的前置节点修改好了,但是我的前置节点的后置却是null--就是我知道我前面是你,

//你不知道你后面是我

pred.next = node;

//返回新建节点

return node;

}

}

//这里如果尾巴节点为null,说明目前还没有线程节点在队列里,需初始化头节点,并把新建节点

//放在头节点的候面

//这里可以看流程图的,B线程节点入队的时候,

enq(node);

return node;

}

//初始化头节点,并把新建的线程节点放入头节点候面。可看流程图B节点入队的时候

private Node enq(final Node node) {

//这个是死循环直到新建节点放入队列尾成功,跳出循环

for (;;) {

//再次判断尾巴节点是不是空,因为可能中途被其他线程新建了

Node t = tail;

if (t == null) { // Must initialize

//CAS创建头节点

if (compareAndSetHead(new Node()))

//初始化头节点也是尾巴节点

tail = head;

}

//头节点被初始化之后,下次循环走这个

else {

//这块和头节点不为空的操作是一样的,新建节点的前置指向尾巴节点

node.prev = t;

//CAS 更新尾巴节点为新建节点

if (compareAndSetTail(t, node)) {

//原来的尾巴节点的后置指向新建节点

t.next = node;

//返回新建节点

return t;

}

}

}

}

新建节点放入队尾之后,看接下来的操作,分析acquireQueued方法,这个方法是我认为不太好理解的地方,包括线程自旋获取锁,

获取失败之后标记前置节点的waitStatus为SIGNAL,然后自身park 停止自旋,前置节点释放掉锁之后,unpark唤醒后置最近的有效节点(实际处理起来并不像说的那么简单),看Doug Lea大师怎么处理的:

//新入队列的节点都会走这一步,从流程图线程B节点入队来分析

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

//死循环也就是自旋,这里每个节点至少自旋一次去获取锁,获取失败后线程走else,会让线程

//park,park后就不再自旋了,等待unpark唤醒

for (;;) {

//获取新建节点的前置

final Node p = node.predecessor();

//如果新建节点的前置为头节点,那么新建节点会再次尝试获取锁

//如果此时锁被释放,成功获取锁,

if (p == head && tryAcquire(arg)) {

//设置新的头节点为新建节点,

//流程图的B线程节点成功获取锁,头节点被放弃,

//新的头节点为B

setHead(node);

//让原来的头节点指向为null ,GC会回收掉。头节点prev本身就是null

p.next = null; // help GC

failed = false;

return interrupted;

}

//1.如果说B线程节点获取锁失败

//2.或者说新建节点的前置不是头节点,可看流程图线程C线程节点入队,他的前置

//节点是B节点

//如果获得锁失败,根据waitStatus决定是否park线程

if (shouldParkAfterFailedAcquire(p, node) &&

//前面执行为true会执行这一步park线程,等待unpark唤醒后,

//检查线程节点线程是否被中断,被中断则返回true

//如果被中断过,阻塞中的线程并不会响应中断,回到acquire()看到

//selfInterrupt() 补偿中断

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

//抛出异常,取消节点操作,出队操作,这块没啥好说的

if (failed)

cancelAcquire(node);

}

}

//主要把当前节点的前置节点waitStatus标记为SIGNAL,释放锁后好唤醒当前节点

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

//如果为SIGNAL也就是-1 说明已经被标记过,直接返回

if (ws == Node.SIGNAL)

/*

* This node has already set status asking a release

* to signal it, so it can safely park.

*/

return true;

//waitStatus>0说明前置节点是已经撤销无效的节点,跳过

//拿流程图的BC来说明,BC都在等待,然后突然B不想等待了,撤销了,那么由谁来唤醒C,

//C就往前继续找有效的前置节点,找到了头节点

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;

}

//这里如果waitStatus没有被修改过,waitStatus默认为0

//或者说小于0又不是SIGNAL,也就只有PROPAGATE

//PROPAGATE在共享锁以及condition队列里使用,这里不做介绍

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.

*/

//CAS 更改前置节点的waitStatus为SIGNAL,这样前置节点释放锁后会唤醒当前节点node

//这里就会发现,从AQS队列操作流程图来看,B节点把A节点的waitStatus置为SIGNAL,

//C节点把B节点的waitStatus置为SIGNAL,这样A节点释放锁,会唤醒B,B执行完释放锁会

//唤醒C

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

加锁到这里就结束了。下面看锁的释放,锁释放比较简单:

//释放锁

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

//成功释放锁后,判断队列中是否有需要唤醒的线程节点

if (h != null && h.waitStatus != 0)

//唤醒下一个有效节点

unparkSuccessor(h);

return true;

}

return false;

}

//子类实现,尝试释放锁

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);

}

//更新state ,这里如果state =0释放锁成功,如果不等于0,

//还有资源没释放(可重入锁机制)

setState(c);

return free;

}

//唤醒节点

private void unparkSuccessor(Node node) {

/*

* If status is negative (i.e., possibly needing signal) try

* to clear in anticipation of signalling. It is OK if this

* fails or if status is changed by waiting thread.

*/

int ws = node.waitStatus;

if (ws < 0)

//这里的ws 一般是-1

//修改当前节点的waitStatus为0就是我可以唤醒别人了

compareAndSetWaitStatus(node, ws, 0);

/*

* Thread to unpark is held in successor, which is normally

* just the next node. But if cancelled or apparently null,

* traverse backwards from tail to find the actual

* non-cancelled successor.

*/

Node s = node.next;

//当前节点后置没有节点或者节点无效

if (s == null || s.waitStatus > 0) {

s = null;

//从队列尾部往前遍历寻找当前节点最近的有效节点

//这里为什么从后往前遍历,因为在加锁的时候,新建节点的时候那个思考?

//回到addWaiter看,三步不是原子操作。有可能头节点的next节点为空。但是其实已经存在了

//只是还没有做pred.next = node这一步,所以从后往前遍历一定会遍历到所以节点

for (Node t = tail; t != null && t != node; t = t.prev)

//找到唤醒的节点t

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

//唤醒s线程继续可以自旋争夺锁

LockSupport.unpark(s.thread);

}

到这里就结束了:分析过程中遇到几个问题。查了很多:park 使当前线程取消自旋(即使在死循环里),一开始对为什么释放锁的时候寻找后置节点从后遍历有疑问,因为感觉从前遍历一下不就找到了吗,分析完之后发现了原因。

这里再说下简述下公平锁吧,公平锁和非公平锁不同就是,公平锁不会直接CAS获取锁,而是先去队列里判断有没有等待的线程,如果有等待的线程,那么直接排在候面,如果只有头节点或者没有等待的线程他会尝试获取锁,主要看hasQueuedPredecessors这个方法,其他的都和公平锁一样了

AQS里面使用了大量的CAS和unsafe类的方法,有兴趣可以了解一下,底层C++实现的

AQS的静态内部类Node 也可以具体了解下

AQS里还有很多没分析到,有在像Semaphore,CountDownLatch里面用到的,后续再去学习,关于AQS 网上太多文章了,我这里就当作自己的一次学习笔记吧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值