AQS源码(二)

手把手教你阅读AQS源码(二)

上篇文章笔者从源码层面总结了ReentrantLock的非公平锁加锁流程,这次继续看下ReentrantLock的公平锁如何加锁,这里放一下链接传送门->非公平加锁源码分析

还是老样子,不能调试的源码尽量不要去读,我们先来搞一个公平锁的调试Demo


public class ReentrantLockDemo {
    public static void main(String[] args) {
    	// 构造方法传入true是公平锁的创建方式
        final ReentrantLock lock = new ReentrantLock(true);
        try {
        	// 公平锁的加锁入口 
            lock.lock();
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
        	// 释放锁
            lock.unlock();
        }
    }
}

ReentrantLock公平锁加锁流程源码分析

场景: t1,t2,t3三个线程争抢给公平锁加锁
这里既然是公平锁的加锁逻辑,那么我们直接去看FairSync中加锁逻辑的实现:

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        // ...
}

这里会调到AQS的acquire(1)方法,跟非公平锁一样只不过会走公平锁尝试加锁的逻辑:

AQS
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

最终走的是FairSync的tryAcquire(1):

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	// hasQueuedPredecessors()这个方法是公平锁加锁的精髓
            	// 主要用来判断要不要排队
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

简单分析一下: t1,t2,t3这三个线程都来竞争这把公平锁,在走到int c = getState()这里的时候,有两种情况:

  • t != 0 ,说明锁被其他线程持有,这里又得去判断是不是重入
    是重入:c + 1 ,再把新的状态设置回去,返回true表示重入加锁成功了
    不是重入:直接返回false,表示加锁失败
  • t == 0 ,说明锁闲置
if (c == 0) {
            	// hasQueuedPredecessors()这个方法是公平锁加锁的精髓
            	// 主要用来判断要不要排队 <<<注意这个方法取反了>>>
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }

hasQueuedPredecessors()这个方法如果返回true,取反后,短路后面加锁的代码,整体返回false,说明这次尝试加锁失败了,需要走后面的逻辑,如果返回false,说明不需要排队,那我就去CAS加锁。(当然不一定,比如t1,t2都判断自己不需要排队,那么CAS肯定只有一个成功,失败的那个需要走后面的逻辑)。这块的逻辑一定要理清。
看下最主要的这个方法hasQueuedPredecessors():

public final boolean hasQueuedPredecessors() {
		// t是队列的头指针
        Node t = tail; // Read fields in reverse initialization order
		// h是队列的尾指针
        Node h = head;
        // 这里声明一个局部变量s 在后面进行赋值,表示的是队列头结点的后继节点
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

第一次看到这段代码感觉,java代码还能如此的牛逼,这段代码真拿一句话来形容真的是我了个蹦擦擦…哈哈。
好了言归正传,上面我们分析了,这行代码最终返回false,调用处取反,可以去CAS加锁,说明不需要排队。

return  h != t && ((s = h.next) == null || s.thread != Thread.currentThread());

我们对这几个条件全排列枚举一下可能有的情况,逐一分析一下:

  • false && (…) 最终的结果是不需要排队。分析: h != t 返回false,说明 h == t 。这里其实有两种情况,①如果队列没有初始化,确实h == t == null ,想一下都没有队列我排什么队,合理。②如果队列初始化了,有一种情况下 h == t != null ,比如队列中有三个线程按先后顺序在等待t1,t2,t3,当t1被唤醒后拿到了锁,t1会把队列头设置为自己,同样的t1执行完后唤醒t2,那么此时t2是对头,当t2执行完后,唤醒t3,那么此时t3是对头,t3之后没人排队了t3也是队尾。想一下这个时候队列里已经没有人在排队了,我还排什么队。合理。因为在AQS中对头这个节点比较特殊,它默认是不参与排队的,为什么?因为AQS的对头要么是持有锁的线程的节点,要么是NULL。原因如下:
    首先AQS的对头是由第一个需要排队的线程创建的空节点,里面啥属性都没有,示意一下大概是这个样子:
    在这里插入图片描述
    此时的头结点是NULL,当t1被头唤醒去获取锁成功后,t1会把前一个节点舍弃,把自己设置为头结点,说明此时头结点是持有锁的线程的节点。
  • true && (true || true)
    h !=t 为true,我们上面也分析了,说明此时队列里至少有一个节点在排队,那么
    s = h.next 肯定不为null,这种情况不存在
  • true && (true || false)
    同上
return  h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
  • true && (false || false)
    这个情况下s.thread == Thread.currentThread(),也就是说当前来排队的线程和当前队列中第一个排队的线程是同一个,那么这种情况下,最终结果返回false,也不需要排队。这个可能理解起来比较绕。什么意思,就好比我们去银行办理业务,就一个服务窗口,大家都在排着队,我来了之后我发现第一个在排队的是我一个好朋友,那我就有资格插队。这样解释起来有点牵强,大概就是这么个意思。
  • true && (false || true)
    那当然这种情况下,就没有资格插队。就得去乖乖排队了。

总结一下,其实这个方法的作用,就是对于公平锁来说,我来争夺锁了,我到底要不要去排队?,不需要排队了,好那我去加锁,需要排队,就去排队咯。

再回到方法的调用处来看,其实后面的逻辑就跟非公平锁的逻辑一模一样了,先入队列,然后park自己等待前驱节点的唤醒。好了至此,公平锁和非公平锁的加锁逻辑我们都看完了,简单画了一个草图,可以再理一下二者的上锁流程。如下:
在这里插入图片描述
总结:非公平锁上来二话不说先CAS,拿不到锁再入队列。公平锁上来必须判断自己要不要排队,不需要排队才CAS去获取锁,获取失败了仍然要入队列。

下一章节,一起看下公平锁和非公平锁是怎么释放锁的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值