ReentrantLock和AQS原理

观摩大佬七八年前的文章:
https://blog.csdn.net/luonanqin/article/details/41871909

ReentrantLock类图:
在这里插入图片描述
AbstractOwnableSynchronizer类保持和获取独占线程。
AbstractQueuedSynchronizer是以虚拟队列的方式管理线程的锁获取与锁释放,以及各种情况下的线程中断。提供了默认的同步实现,但是获取锁和释放锁的实现定义为抽象方法,由子类实现。目的是使开发人员可以自由定义获取锁以及释放锁的方式。
Sync是ReentrantLock的内部抽象类,实现了简单的获取锁和释放锁。
NonfairSync和FairSync分别表示“非公平锁”和“公平锁”,都继承于Sync,并且都是ReentrantLock的内部类。
ReentrantLock实现了Lock接口的lock-unlock方法,根据fair参数决定使用NonfairSync还是FairSync。
这里面有两个重点内容:
也就是AQS
AbstractQueuedSynchronizer内部的Node
AbstractQueuedSynchronizer内部的state

Node:
在这里插入图片描述
Node是对每一个访问同步代码的线程的封装。不仅包括了需要同步的线程,而且也包含了每个线程的状态,比如等待解除阻塞,等待条件唤醒,已经被取消等等。同时Node还关联了前驱和后继,即prev和next。个人认为是为了以集中的方式管理多个不同状态的线程,当不同的线程发生状态改变时,可以尽快的反应到别的线程上,提高运行效率。比如某个Node的prev已经被取消了,那么当对这个prev解除阻塞的时候就可以被忽略掉,进而尝试解除该Node的阻塞状态。

多个Node连接起来成为了虚拟队列(因为不存在真正的队列容器将每个元素装起来所以说是虚拟的,我把它称为release队列,意思是等待释放),那么就得有head和tail。针对公平锁,head是不带线程的特殊Node,只有next,而最新一个请求锁的线程取锁失败时就把它添加到队尾,即tail。但是对于非公平锁,新请求锁的线程会插队,也许会插到最前面,也许不会。

这里可能有人会有疑问:head放在队列中有什么用处?为什么不是一个等待锁的线程作为head呢?原因很简单,因为每个等待线程都有可能被中断而取消,对于一个已经取消的线程,自然是有机会就把它gc了。那么gc前一定得让后续的Node成为head,这样一来setHead的操作过于分散,而且要应对多种线程状态的变化来设置head,这样就太麻烦了。所以这里很巧妙地将head的next设置为等待锁的Node,head就相当于一个引导的作用,因为head没有线程,所以不存在“取消”这种状态。

在这里插入图片描述
state是用来记录锁的持有情况。
没有线程持有锁的时候,state为0。
当某个线程获取锁时,state的值增加,具体增加多少开发人员可自定义,默认为1,表示该锁正在被一个线程占有。
当某个已经占用锁的线程再次获取到锁时,state再增长,此为重入锁。
当占有锁的线程释放锁时,state也要减去当初占有时传入的值,默认为1。
多个线程竞争锁的时候,state必须通过CAS进行设置,这样才能保证锁只能有一个线程持有。当然这是排它锁的规则,共享锁就不是这样了。

公平锁和非公平锁的不同仅在于修改state的时机。看下面的代码就能明白:

// ReentrantLock.class
 
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 此为公平锁的实现,而非公平锁不调用hasQueuedPredecessors方法,即不需要判断队列里是否有内容,直接通过CAS修改state来竞争锁
        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;
}

基本上把Node和state的意义弄明白了,一个正常的lock-unlock过程应该很容易明白。但是代码里有很多地方要针对已取消的线程做特殊处理,所以理解上还是会有困难。因为已经有几篇文章进行了源码讲解,所以这里我就不再把每段源代码都拿出来细讲了。

先看看 lock()的流程:(图中的Node0和Node1在源代码中不存在,是我为了方便说明清楚才添加的别称)
在这里插入图片描述
如图所示,彩色的字都是一个CAS操作,其中三个红色CAS都对成功和失败有相应地处理,为什么另外一个蓝色CAS不关心设置是否成功呢?下面这段代码里我给出了解释:

// AbstractQueuedSynchronizer.class
 
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    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;
    } 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.
         */
        /*
         * 为什么不关心是否成功却还要设置呢?
         *
         * 如果设置失败,表示前驱已经被signal了。如果前驱是head,说明有机会获取锁,所以返回false后还可以再次tryAcquire
         *
         * 如果设置成功,表示前驱等待signal。如果再次确认pred.waitStatus仍然是Node.SIGNAL,则表明前驱等待释放锁的情况下必须阻塞当前线程
         * 所以返回true后即被park
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

再看下 unlock()的流程图:
在这里插入图片描述
如图所示,这里的粉红色折线与lock流程图里的粉红色虚折线对应,即线程A调用lock阻塞与线程B调用unlock解除线程A的阻塞。同时可以看到unlock只有一个CAS操作,但是也不用关心设置是否成功。我给这段代码做了下面解释:

// AbstractQueuedSynchronizer.class
 
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;
	/*
	 * 为什么不关心是否成功却还要设置呢?
	 *
	 * 注意这里的Node实际就是head
	 * 
	 * 如果设置成功,即head.waitStatus=0,则可以让这时即将被阻塞的线程有机会再次调用tryAcquire获取锁。
	 * 也就是让shouldParkAfterFailedAcquire方法里的compareAndSetWaitStatus(pred, ws, Node.SIGNAL)执行失败返回false,这样就能再有机会再tryAcquire了
	 *
	 * 如果设置失败,新跟随在head后面的线程被阻塞,但是没关系,下面的代码会立即将这个阻塞线程释放掉
	 */
    if (ws < 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;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

尽可能利用CAS操作减少阻塞的机会,让线程能有更多机会获取锁,毕竟阻塞线程是内核操作,开销不小。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值