如果你在之前的文章中我们已经了解过了 java中的管程sychronized实现原理,那么再来看AbstractQueuedSynchronizer的实现就会非常简单了,虽然使用的加锁技术有不同之处,但是他们都是基于同样的理念去实现的.

打开ReentrantLock 的源码你会发现,ReentrantLock 的所有操作都是基于Sycn对象去操作的,而Sycn继承于AbstractQueuedSynchronizer的一个子类,而不管是NonfairSync非公平锁,还是FairSync 公平锁都继承于Sycn,这也说明了ReentrantLock 里面最终维护的其实就是一个AQS对象。

img

1、AQS的初始化。

当我们调用new ReentrantLock()时其实就是初始化了一个AQS对象,该对象包括如下几个属性

1、state (锁的状态 0代表未加锁,>0则代表已加锁,和重入次数)

2、exclusiveOwnerThread (拥有当前锁的线程)

3、由 Node节点组成的双向链表,Node节点保存了等待线程的相关信息。

img

当AQS初始化之后,会初始化一个如下对象

1、state状态为0

2、exclusiveOwnerThread =null

3、由head 和tail两个空节点组成的首尾双向链表。

img

2、AQS的加锁过程

一个线程通过ReentrantLock .lock() 首先会获得当前AQS锁的状态,然后根据锁的对应状态做出不同的处理,具体分为以下几种情况;

1、第一种情况:初次加锁,还没有任何线程获得AQS的锁;

2、第二种情况:已经有线程获得了AQS的锁,但是加锁的线程和当前线程是同一个;

3、第三种情况三:已经有线程获得了AQS的锁,并且加锁的线程和当前线程不是同一个;

下面我们针对每种情况进行分析

2-1、 第一种情况:初次加锁,还没有任何线程获得AQS的锁;

线程A调用ReentrantLock .lock() 进行加锁,当还没有任何对象获得AQS锁时候会执FairSync.tryAcquire()方法的如下代码:

img

以上标注代码的逻辑:

1、判断队列中是否有正在等待锁的节点,因为是初次加锁,所以这里head和tail节点都是空;

2、使用compareAndSetState()方法对AQS进行加锁(此方法能保证操作的原子性)。

3、设置当前获得锁对象的线程;

img

2-2、第二种情况:已经有线程获得了AQS的锁,但是获得锁的线程和当前线程是同一个;

img

如上面代码,当线程A调用demo1()方法,已经获得了AQS锁,当调用demo2时又会去竞争AQS锁,这样允许同一个线程多次获得同一把锁的情况称为 “可重入锁”

当线程A再次调用ReentrantLock .lock() 时会执行FairSync.tryAcquire()对应重入锁逻辑的代码:

img

以上标注代码的逻辑:

1、首先拿当前线程和已经获得AQS锁的线程对比是否是同一个线程;

2、是同一个线程的话获得 当前AQS的state ,执行state+1累计重入次数;

3、修改AQS的state;

最后执行结果如下图:

img

2-3、第三种情况:已经有线程获得了AQS的锁,并且加锁的线程和当前线程不是同一个;

当前面的线程A已经获得了AQS锁,还没进行释放,此时线程B调用ReentrantLock .lock() 方法获取锁会执行AbstractQueuedSynchronizer.acquire()的如下代码:

img

以上代码逻辑:

1、首先执行 addWaiter()把获得锁不成功的线程加入到阻塞队列

A、把线程B封装为一个Node节点(这里我们定义为nodeB);

B、如果当前AQS中双向链表tail节点不为空,则把nodeB设置为tail节点,把nodeB.pre指向原来的tail节点,并把原来的tail节点的nex指向nodeB;

C、如果当前AQS中双向链表tail节点为空,则说明当前链表里面没有其他等待的节点,那么首先创建一个Node 节点(这里定义为nodeN)作为head节点,然后把nodeN.nex指向nodeB节点,把tail指向nodeB节点,nodeB.pre指向nodeN节点;

2、addWwaiter()成功后调用 acquireQueued()方法

F、首先会再次尝试获取一下锁;

G、当获取锁失败后,把双向链表中nodeB.pre指向的节点的waitStatus 设置为 -1;

注:

waitStatus=0 新加的节点,处于阻塞状态。

waitStatus= 1 表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。

waitStatus=-1 表示该线程的后续线程需要阻塞,即只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程 。

waitStatus=-2 表示该节点的线程处于等待Condition条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal),设置其值为0,重新进入阻塞状。

waitStatus=-3 表示该线程以及后续线程进行无条件传播(CountDownLatch中有使用)共享模式下, PROPAGATE 状态的线程处于可运行状态。

因为我们的案例是第一加锁,所以head 和tail节点都为null ,所以代码会走A、C、F、G逻辑 。

最后结果如图。

img

最后整个AQS初始化、线程A两次加锁、线程B加锁的流程如图:

img

3、AQS的解锁过程

通过上面的流程,我们已经了解了AQS的加锁过程,当线程加锁不成功之后,会把当前线程放到一个等待队列中去,这个队列是由head和tail构建出来的一个双向链表,下面我们继续上面的案例继续分析AQS的解锁过程。

因为上面AQS的锁获得线程为线程A ,所以现在只有线程A可以进行释放锁,当线程A调用ReentrantLock .unlock() 时,最终执行ReentrantLock.tryRelease()方法,代码如下:

img

1、获得当前AQS的state 并进行减1(state每减1代表释放一次锁);

2、当state=0的时候说明当前锁已经完全释放了,此时会设置拥有AQS 锁的线程为null;

3、当state不等于0说明锁还没有释放完全,此时修改state的值。

因为上面案例中线程A获得了2次锁,所以线程A需要调用两次ReentrantLock .unlock()才能释放锁,整个流程如下图:

img

当线程A释放完锁之后程序会调用AbstractQueuedSynchronizer.release()方法的如下代码:

img

我想如果明白了AQS的加锁过程,那么你已经猜到了,当线程释放锁完毕之后接下来肯定是唤醒等待队列里面的线程了,这段代码也的确是在做这些事情:

1、获得等待队列链表中的head节点;

2、当head节点不为空,并且head节点的waitStatus!=0(这里代表线程状态正常)时,调用unparkSuccessor()方法唤醒链表中head.next节点中的线程,。

3、当前链表里面head.next 为nodeB,所以线程B会被唤醒,然 后重新去获取锁,同时重构链表节点.

最后结果如图:

img

4、公平锁非公平锁的区别

公平锁进行lock()的时候,如果AQS为无锁状态,公平锁首先会判断AQS里面是否有等待的线程,如果有的话会添加到队列里面排队,队列里面没有线程的话才会去尝试获取锁。

非公平锁 进行lock()的时候,只要是AQS是无锁状态,不管队列里面是否有等待线程都会直接去尝试获得锁。