https://segmentfault.com/a/1190000015804888
一、简介
本章以RenntrantLock的调用为例,说明AbstractQueuedSynchronizer提供的独占功能。
本章结构如下:
1、以ReentrantLock的公平策略为例,分析AQS的独占功能。
2、以ReentrantLock的非公平策略为例,分析AQS的独占功能
3、分析AQS的锁中断、限时等待功能。
二、ReentrantLock的公平策略原理
本节对ReentrantLock公平策略的分析基于以下示例:
假设现在有3个线程:ThreadA、ThreadB、ThreadC,一个公平的独占锁,3个线程会依次尝试去获取锁:
ReentrantLock lock = new ReentrantLock(true);
线程的操作时序如下:
//ThreadA lock
//ThreadB lock
//ThreadC lock
//ThreadA release
//ThreadB release
//ThreadC release
2.1、ThreadA首先获取到锁
ThreadA首先调用ReentrantLock的lock方法,我们看下该方法的内部:
最终其实调用了FairSync的lock方法:
acquire方法来自AQS:
其中tryAcquire方法需要AQS的子类自己去实现,我们来看下ReentrantLock中的实现:
可以看到,在ReentrantLock中,同步状态State的含义如下:
State | 资源的定义 |
---|---|
0 | 表示锁可用 |
1 | 表示锁被占用 |
大于1 | 表示锁被占用,且值表示同一线程的重入次数 |
ThreadA是首个获取锁的线程,所以上述方法会返回true,第一阶段结束。(ThreadA一直保持占用锁的状态)
此时AQS中的等待队列还是空:
2.2ThreadB开始获取锁
终于,ThreadB要登场了,一样,ThreadB先去调用lock方法,最终调用了AQS的acquire方法:
tryAcquire方法肯定是返回false(因为此时ThreadA占有着锁)。
接下来看下addWaiter方法,这个方法其实就是将当前调用线程包装成一个【独占结点】,添加到等待队列尾部。
这里关键是enq方法,因为并发插入的情况存在,所以该方法设计成了自旋操作,保证结点能成功插入,具体步骤如下:
①当队列为空时,先创建一个dummy头结点;
②进入下一次循环,插入队尾结点;
好了,ThreadB已经被包装成结点插入队尾了,接下来会调用acquireQueued方法,这也是AQS中最重要的方法之一:
在AQS中,等待队列中的线程都是阻塞的,当某个线程被唤醒时,只有该线程是首结点(线程)时,才有权去尝试获取锁。
上述方法中,将ThreadB包装成结点插入队尾后,先判断ThreadB是否是首结点(注意不是头结点,头结点是个dummy结点),发现确实是首节点(node.prodecessor==head),于是调用tryAcquire尝试获取锁,但是获取失败了(此时ThreadA占有着锁),就要判断是否需要阻塞当前线程。
注意,对于独占功能,只使用了3种结点状态:
CANCELLED | 1 | 取消。表示后驱结点被中断或超时,需要移出队列 |
SIGNAL | -1 | 发信号。表示后驱结点被阻塞了(当前结点在入队后、阻塞前,应确保将其prev结点类型改为SIGNAL,以便prev结点取消或释放时将当前结点唤醒。) |
CONDITION | -2 | Condition专用。表示当前结点在Condition队列中,因为等待某个条件而被阻塞了 |
对于在等待队列中的线程,如果要阻塞它,需要确保将来有线程可以唤醒它,AQS中通过将前驱结点的状态置为SIGNAL:-1来表示将来会唤醒当前线程,当前线程可以安心的阻塞。
待续。。。