走进源码学习Lock和AQS(AbstractQueuedSynchronizer)怎么配合实现加锁流程

JUC:java.util.concurrent的缩写,Java的并发编程类都在这个包下面。
AQS:AbstractQueuedSynchronizer抽象类的缩写,Lock、Condition的具体实现都在这个类,很重要的一个类。


主要类结构:

AbstractOwnableSynchronizer的主要结构:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    // 当前持有锁的线程
	private transient Thread exclusiveOwnerThread;
}

AQS(AbstractQueuedSynchronizer) 的主要结构:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
	/**
     * 队列头
     */
    private transient volatile Node head;
	/**
     * 队列尾
     */
    private transient volatile Node tail;

    /**
     * 锁状态:加锁成功则为1,重入+1,解锁则为0
     */
    private volatile int state;
}

Node节点类的主要结构:

里面封装了上一个节点、下一个节点和线程,某种意义上Node就等于一个线程。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

	static final class Node {
		volatile int waitStatus;
		volatile Node prev;
		volatile Node next;
		volatile Thread thread;
		Node nextWaiter;
    }

}

ReentrantLock类的主要结构:

public class ReentrantLock implements Lock, java.io.Serializable {

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
    
	    public void lock() {
	        sync.lock();
	    }
	    
	    public boolean tryLock() {
	        return sync.nonfairTryAcquire(1);
	    }
	    
	    public Condition newCondition() {
	        return sync.newCondition();
	    }
	    
		public void unlock() {
	        sync.release(1);
	    }
    }


ReentrantLock.Sync的继承关系:

AQS中的队列关系图

在这里插入图片描述

查看源码了解加锁流程

现在我们来查看源码,了解加锁的过程。

构造方法

日常开发使用锁的代码示例:


ReentrantLock类提供了无参和有参两种构造方式,不传默认等于false,非公平锁。为true表示公平锁。
对应源码:

public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们先以公平锁加锁为入口来看看加锁的代码实现。

lock()

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

“1”标识加锁成功之后改变的值,对应AQS的state,int类型默认为0。

acquire()

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

acquire()方法首先会调用tryAcquire方法,注意tryAcquire的结果做了取反。
tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued()方法加入队列去排队,如果加锁成功则不会调用,此流程结束。

tryAcquire()尝试加锁

/*
 *如果c不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
*如果c不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
*那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
*/
protected final boolean tryAcquire(int acquires) {
    //获取当前线程
    final Thread current = Thread.currentThread();
    //获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
    int c = getState();
    if (c == 0) {//没人占用锁--->我要去上锁
        //hasQueuedPredecessors()判断自己是否需要排队,如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程,继而返回true
        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;
}

“tryAcquire(arg)”尝试拿锁,第一次进来拿锁成功,修改AQS的属性,CAS修改state=1,exclusiveOwnerThread=currentThread,然后返回true。
if (!tryAcquire(arg)…… 这里取反后if条件为false,此流程结束。

hasQueuedPredecessors()判断自己是否需要排队

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    /**
     * 不需要排队有三种情况
     * 一:队列没有初始化,不需要排队。
     * 直接去加锁,但是可能会失败;为什么会失败呢?
     * 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;有一个必然失败
     * 比如t1先拿到锁,那么另外一个t2则会CAS失败,这个时候t2就会去初始化队列acquireQueued(),并排队
     *
     * 二:队列被初始化了,但是tc过来加锁,发觉队列当中第一个排队的就是自己;比如重入;
     * 
     * 三:持有的锁正好被释放,自己是队列中第一个排队的,正在排队时会先去尝试获取一下锁,因为有可能这个时候持有锁锁的那个线程可能释放了锁;
     * 如果释放了就直接获取锁执行。但是如果没有释放他就会去排队,
     * 所以这里的不需要排队,不是真的不需要排队
     *
     * h != t 判断首不等于尾这里要分三种情况
     * 1、队列没有初始化,也就是第一个线程tf来加锁的时候那么这个时候队列没有初始化,
     * h和t都是null,那么这个时候判断不等于则不成立(false)那么由于是&&运算后面的就不会走了,
     * 直接返回false表示不需要排队,而前面又是取反(if (!hasQueuedPredecessors()),所以会直接去cas加锁。
     *
     * 2、队列被初始化了,后面会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;(不绝对,还有第3中情况)
     * 大于1个数据则成立;继续判断把h.next赋值给s;s有是对头的下一个Node,
     * 这个时候s则表示他是队列当中参与排队的线程而且是排在最前面的;
     * 为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;下文有详细解释
     * 因为h也就是对头对应的Node对象或者线程他是持有锁的,但是不参与排队;
     * 这个很好理解,比如你去买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
     * 队列里面的h是不参与排队的这点一定要明白;参考下面关于队列初始化的解释;
     * 因为h要么是虚拟出来的节点,要么是持有锁的节点;什么时候是虚拟的呢?什么时候是持有锁的节点呢?下文分析
     * 然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;
     * 假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个Node的时候h.next肯定不为空;
     * 由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况
     *        2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;
     *              那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true  最后true)
     *              结果: true && true 方法最终放回true,所以需要去排队
     *              其实这样符合情理,试想一下买火车票,队列不为空,有人在排队;
     *              而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队
     *        2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程
     *             这个时候整体结果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最后false)
     *            结果:true && false 方法最终放回false,所以不需要去排队
     *            不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;
     *            这里又分为两种情况(日了狗了这一行代码;有同学课后反应说子路老师老师老是说这个AQS难,
     *            你现在仔细看看这一行代码的意义,真的不简单的)
     *             2.2.1  第一种情况加锁成功?有人会问为什么会成功啊,如这个时候h也就是持有锁的那个线程执行完了
     *                      释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 自己看代码
     *             2.2.2  第二种情况加锁失败?有人会问为什么会失败啊。假如这个时候h也就是持有锁的那个线程没执行完
     *                       没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if(else if是相对于 if (c == 0)的)
     *                      那么如果失败怎么办呢?后面分析;
     *
     *----------第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是有个插曲;
     * ----------他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放
     *----------也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;
     *----------如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);
     * --------- 你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的
     * 疑问:比如如果在在排队,那么他是park状态,如果是park状态,自己怎么还可能重入啊。
     * 希望有同学可以想出来为什么和我讨论一下,作为一个菜逼,希望有人教教我
     *  
     * 
     * 3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?ts加锁的时候里面就只有一个数据?
     * 其实不是,因为队列初始化的时候会虚拟一个h作为头结点,tc=ts作为第一个排队的节点;tf为持有锁的节点
     * 为什么这么做呢?因为AQS认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,
     *  而ts其实需要排队的,因为这个时候tf可能没有执行完,还持有着锁,ts得不到锁,故而他需要排队;
     * 那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,
     * 故而ts无法排在tf之后,只能虚拟一个thread=null的节点出来(Node对象当中的thread为null);
     * 那么问题来了;究竟什么时候会出现队列当中只有一个数据呢?假设原队列里面有5个人在排队,当前面4个都执行完了
     * 轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个
     * 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了;
     * 所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点
     * 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析
     * 记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁
     * h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队
     *
     *
     *-------------第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,
     *-------------这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas;尝试加锁
     *-------------这是AQS的设计原理,他会判断你入队之前,队列里面有没有人排队;
     *-------------有没有人排队分两种情况;队列没有初始化,不需要排队
     *--------------队列初始化了,按时只有一个节点,也是没人排队,自己先也不排队
     *--------------只要认定自己不需要排队,则先尝试加锁;加锁失败之后再排队;
     *--------------再一次解释了不需要排队这个词的歧义性
     *-------------如果加锁失败了,在去park,下文有详细解释这样设计源码和原因
     *-------------如果持有锁的线程释放了锁,那么我能成功上锁
     *
     **/
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}


参考此图理解:

非公平锁的拿锁流程:

获取锁的差异,如下图:
)
跟公平锁比,多了红色框框的代码,即先抢一次,抢不到再执行acquire(1)走常规流程。
在这里插入图片描述
公平锁的上锁是必须判断自己是不是需要排队;而非公平锁是直接进行CAS修改计数器看能不能加锁成功;如果加锁不成功则乖乖排队(调用acquire);所以不管公平还是不公平;只要进到了AQS队列当中那么他就会排队;一朝排队,永远排队。


流程大概过了一遍,从流程中看出来的设计思路:
1、如果线程是交替执行的,那么AQS队列不会初始化,减少不必要的开销;(示例一)
2、线程之间有资源竞争,但时间相隔很短,队列会初始化,拿不到锁的线程会入队,但不会阻塞,会自旋尝试拿锁,减少park、unpark;(示例二第一种情况)
3、线程之间有资源竞争,但时间相隔很长,队列会初始化,拿不到锁的线程会入队,且阻塞等待唤醒;(示例二第二种情况)


Lock和AQS实现代码还是有点复杂的,因为Doug Lea会考虑多种情况且兼顾性能。比如队列的初始化,排队前再次尝试拿锁等等。文章还会继续补充,我也在边写这篇文章边消化。


参考文章:
https://blog.csdn.net/java_lyvee/article/details/98966684

猜你喜欢

转载自blog.csdn.net/WLQ0621/article/details/107481956