扫描下方二维码或者微信搜索公众号菜鸟飞呀飞,即可关注微信公众号,阅读更多Spring源码分析和Java并发编程文章。
在阅读本文之前可以先思考一下以下两个问题
1. ReentrantLock是如何在Java层面(非JVM层面)实现锁的?2. 什么是公平锁?什么是非公平锁?在AQS的acquire()方法中会先调用子类的tryAcquire()方法,此时由于我们创建的是公平锁,所以会调用FairSync类中的tryAcquire()方法。(关于acquire()方法的详细介绍,可以参考:队列同步器(AQS)源码分析)。
tryAcquire()方法的作用就是获取同步状态(也就是获取锁),如果当前线程成功获取到锁,那么就会将AQS中的同步状态state加1,然后返回true,如果没有获取到锁,将会返回false。FairSync类上tryAcquire()方法的源码如下。
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 获取同步状态state的值(在AQS中,state就相当于锁,如果线程能成功修改state的值,那么就表示该线程获取到了锁) int c = getState(); if (c == 0) { // 如果c等于0,表示还没有任何线程获取到锁 /** * 此时可能存在多个线程同时执行到这儿,均满足c==0这个条件。 * 在if条件中,会先调用hasQueuedPredecessors()方法来判断队列中是否已经有线程在排队,该方法返回true表示有线程在排队,返回false表示没有线程在排队 * 第1种情况:hasQueuedPredecessors()返回true,表示有线程排队, * 此时 !hasQueuedPredecessors() == false,由于&& 运算符的短路作用,if的条件判断为false,那么就不会进入到if语句中,tryAcquire()方法就会返回false * * 第2种情况:hasQueuedPredecessors()返回false,表示没有线程排队 * 此时 !hasQueuedPredecessors() == true, 那么就会进行&&后面的判断,就会调用compareAndSetState()方法去进行修改state字段的值 * compareAndSetState()方法是一个CAS方法,它会对state字段进行修改,它的返回值结果又需要分两种情况 * 第 i 种情况:对state字段进行CAS修改成功,就会返回true,此时if的条件判断就为true了,就会进入到if语句中,同时也表示当前线程获取到了锁。那么最终tryAcquire()方法会返回true * 第 ii 种情况:如果对state字段进行CAS修改失败,说明在这一瞬间,已经有其他线程获取到了锁,那么if的条件判断就为false了,就不会进入到if语句块中,最终tryAcquire()方法会返回false。 */ if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 当前线程成功修改了state字段的值,那么就表示当前线程获取到了锁,那么就将AQS中锁的拥有者设置为当前线程,然后返回true。 setExclusiveOwnerThread(current); return true; } } // 如果c等于0,则表示已经有线程获取到了锁,那么这个时候,就需要判断获取到锁的线程是不是当前线程 else if (current == getExclusiveOwnerThread()) { // 如果是当前线程,那么就将state的值加1,这就是锁的重入 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); // 因为此时肯定只有一个线程获取到了锁,只有获取到锁的线程才会执行到这行代码,所以可以直接调用setState(nextc)方法来修改state的值,这儿不会存在线程安全的问题。 setState(nextc); // 然后返回true,表示当前线程获取到了锁 return true; } // 如果state不等于0,且当前线程也等于已经获取到锁的线程,那么就返回false,表示当前线程没有获取到锁 return false; } } 在tryAcquire()方法中,先判断了同步状态state是不是等于0。1. 如果等于0,就表示目前还没有线程持有到锁,那么这个时候就会先调用hasQueuedPredecessors()方法判断同步队列中有没有等待获取锁的线程,如果有线程在排队,那么当前线程肯定获取锁失败(因为AQS的设计的原则是FIFO,既然前面有人已经在排队了,你就不能插队,老老实实去后面排队去),那么tryAcquire()方法会返回false。如果同步队列中没有线程排队,那么就让当前线程对state进行CAS操作,如果设置成功,就表示当前获取到锁了,返回true;如果CAS失败,表示在这一瞬间,锁被其他线程抢走了,那么当前线程就获取锁失败,就返回false。2. 如果不等于0,就表示已经有线程获取到锁了,那么此时就会去判断当前线程是不是等于已经持有锁的线程(getExclusiveOwnerThread()方法的作用就是返回已经持有锁的线程),如果相等,就让state加1,这就是所谓的重入锁,重入锁就是允许同一个线程多次获取到锁。3. state既不等于0,当前线程也不等于持有锁的线程,那么就返回false,表示当前线程获取到锁失败。就这样,通过FairSync类的tryAcquire()方法,就实现了公平锁获取锁的逻辑。在不同的锁的实现中,tryAcquire()方法的逻辑是不一样的,例如在非公平锁中,NonfairSync类的tryAcquire()中,代码逻辑和FairSync类的tryAcquire()方法就不一样,在非公平锁的实现中,当线程尝试对state进行CAS操作之前,没有对同步队列中有没有线程在排队进行判断(即没有调用hasQueuedPredecessors()方法)。hasQueuedPredecessors()方法的作用是判断同步队列中有没有线程在排队,如果有,就返回true,如果没有,就返回false。其源码如下,在贴出的代码片段中,对该方法进行了详细的解释(要看懂该方法,需要对AQS的设计原理有一定的了解,可以阅读这一篇文章:队列同步器(AQS)的设计原理)。 public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; /** * 该方法的作用是判断队列中是否有线程在排队,返回true表示有线程排队,返回false表示没有线程排队 * * 首先判断 h!=t,即判断头结点和尾结点是否相等,头结点和尾结点相等只有两种情况, * 第一:队列还没有被初始化,此时head = null, tail = null,此时线程不需要排队。 * 第二:已经有一个线程获取到了锁,当第二个线程进来获取锁时,因为获取不到锁,所以会被需要入队,入队之前它需要先初始化队列, * 在初始化时,在enq()方法中,第一步是先将tail = head,然后第二步再将当前线程代表的Node设置为tail。所以在这两步操作 * 的中间的一瞬间,是存在tail = head这个可能性的。此时对所有线程而言是也不需要排队 * 当 h!=t 的结果为true时,接下来会判断((s = h.next) == null || s.thread != Thread.currentThread()),先判断(s = h.next) == null * * 判断(s = h.next) == null 时,先令 s = h.next ,表示获取到头结点的的下一个节点,即第二个节点,如果 s == null 则表示第二个节点为null,由于前面已经判断了h!=t, * 说明此时已经有第二个线程进入到了队列中,只不过它还没来及将head节点的next指针的指向修改,所以此时线程线程需要排队, * 因为||运算的短路原因,当(s = h.next) == null 的结果为true时,就不会进入到后面的判断了,而此时hasQueuedPredecessors()会返回true,表示线程需要排队。 * 当s不为null时,会进行s.thread != Thread.currentThread() 的判断 * 它会判断s节点中的线程是否不等于当前线程,如果不等于当前线程,hasQueuedPredecessors()会返回true,说明当前线程需要排队。 * 因为当第二个节点不是当前线程,那么就说明当前线程应该至少是排在队列中的第三位,那么它需要排队。 * 如果s节点中的线程等于当前线程,那么说明当前线程是排在队列中的第二位(第一位是已经获取到锁的线程),此时线程是不需要排队的, * 因为可能在这一瞬间已经获取到锁的线程释放了锁,那么排在队列中第二位的线程还排啥子队哦,直接去尝试获取锁即可。 * * * */ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }