此篇文章只分析了ReentrantLock对象获得锁的一个过程。
既然是结合ReentrantLock分析AQS,首先说明一下ReentrantLock的基本组成。
- RenntrantLock中有一个静态抽象内部类Sync,它继承了AbstractQueuedSynchronizer也就是AQS,Sync中有一个抽象方法lock() 和 一个已经实现的方法 nonfairTryAcquire。
- RenntrantLock中有一个成员变量sync,用于引用具体的锁对象。
- ReentrantLock中还有两个静态内部类 NonfairSync 和 FairSync ,它们都继承了Sync类,并实现了上述的抽象方法lock() 和重写了AbstractQueuedSynchronizer中的tryAcquire方法。
- ReentrantLock有非公平锁和公平锁之分,也就是上述的NonfairSync 和 FairSync,在ReentrantLock通过父类Sync去引用子类实例(也就是上述的sync引用),而ReentrantLock中具体使用哪种锁,取决你使用的构造方法。
- ReentrantLock有一个状态值state,0表示ReentrantLock对象是空闲的,大于1是被占用的
分析了ReentrantLock基本组成之后,接下来就开始分析源码了。
1.生成一个非公平的ReentrantLock对象,所以后续的都是基于非公平锁进行分析的。
// 自定义一个对象,注意这里使用的构造方法
private ReentrantLock lock = new ReentrantLock();
// 这是ReentrantLcok中提供的两种构造方法
// 无参数,默认使用非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 带参,具体使用哪种锁由自己决定
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2. 确定使用了哪种锁之后,看实际使用ReentrantLock的lock方法的过程。ReentrantLock实际是有Sync完成的,而Sync中的lock是一个抽象方法,所以具体是由其子类完成的,也就是说使用ReentrantLock的lock方法实际上就是使用的NonfairSync的lock方法。
// ReentrantLock中的lock源码,这里的sync实际指向的就是在构造方法中生成的具体锁对象
public void lock() {
sync.lock();
}
3.知道lock的调用流程之后,我们开始分析nonfairSync的lock方法。首先就是进行CAS操作,如果CAS成功说明此时这个线程获得锁成功了(注意文章开始部分对ReentrantLock的组成说明),直接结束lock就好,但是如果获得锁失败了则进行acquire(1)操作,acquire(1)方法是由AbstractQueuedSynchronizer实现的,从这里开始就开始涉及到AQS了(其实compareAndSetState方法也是由AQS实现的)。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
4.至此,假设第一次获得锁失败,我们开始分析AQS的acquire方法。下面是AQS中的源码,一共涉及到了4个方法,按顺序逐个分析。首先是分析tryAcquire方法,你看AQS源码会发现这个方法其实AQS也实现了,虽然只是抛出一个异常。因为子类对这个方法进行了重写,Java类之间的多态性体现,这里的tryAcquire方法实际是调用的子类的tryAcquire方法(还是注意文章开始的说明,非公平锁重写了tryAcquire方法)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
5.于是先分析tryAcquire方法。看源码很简单,就是调用nonfairTryAcquire方法,这个方法由静态抽象类Sync实现,也就是NonfairSync的父类。nonfairTryAcquire的实现逻辑很简单,主要是进行了两个操作,一个是当ReentrantLock是空闲的时候再次竞争锁,不是空闲的判断此时拥有锁的线程是不是自己的线程,是的话进行可重入操作(ReentrantLock 的可重入性就体现在这里)。至此,如果获得锁成功返回true,回看第四步源码,acquire方法就可以结束了,但是这里为了继续向下分析,假设这里还是获得锁失败,返回false
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// Sync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
6.至此tryAcquire终于分析完了,回到第四步, 开始分析下一个方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),因为这里涉及到两个方法,我们先分析里面的addWaiter(Node.EXCLUSIVE), arg) (注:arg在整个过程中都是1)。下面addWaiter的源码,addWaiter的作用就是一个:将线程封装成节点,追加到同步队列的尾部,并且一定追加成功。addWaiter的方法很好看懂就是判断此时同步队列是不是空的,不是空的就进行一次CAS操作,将node追加到尾部,如果此时队列为空,或者CAS失败则就交给enq方法去实现了。Node.EXCLUSIVE表示独占锁
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
7.这里就要分析enq方法了。(分析源码的过程有时候是挺无聊的,尤其是一环套一环)。从下面的源码可以看出,enq其实有一个自旋操作,enq的主要职责就两个,一个是当同步队列为空的时候初始化同步队列,还有一个是确保封装的node一定能够追加到同步队列的尾部,因为这是一个自旋操作,不成功不结束啊。(自己原来有一个疑问,为什么初始化的操作不直接在addWaiter中完成,而是放到enq之中,后来发现,初始化也是要进行CAS操作的,但是CAS不一定成功,也是需要不停的自旋的,所以将追加节点所需要自旋的操作重新封装到enq中了(纯属个人理解,猜测。。。))
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
8.至此,addWaiter 分析完了,再一次回到第四步,开始分析 acquireQueued(node,1),这个方法感觉是最难理解的一个方法了。总的来说这个方法的作用判断这个节点的前一个节点是不是头结点,是则再次去尝试获得锁,不是则进行一系列操作(详情见shouldParkAfterFailedAcquire的源码),就是确保这个节点的前一个节点的状态一定为SINGLE,这里解释一下SINGLE的意思,SINGLE表示当我这个节点释放锁或者被取消之后,我要负责通知我后面去节点退出阻塞去竞争锁。在确保了这个节点的前一个节点是SINGLE之后,这个线程就会被阻塞掉,然后知道被唤醒,重新开始这里的自旋操作。所以acquireQueued方法的作用总的说就是确保同步队列中节点中的线程有机会去获得独占锁。因为这里的自旋,可以看去它是一定会获得锁的,只是早晚而已。注意这里会返回一个Interrupted值用来记录这个线程是否被中断过,以对应第四步的acquire源码中的selfInterrupt()操作。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
至此,整个lock的过程分析完了,AQS中的主要方法也分析了一个大概,一直说ReentrantLock的lock方法是一定会获得锁的,为什么这么说,从第8步的说明可以得出结论。以上就是自己对lock过程,ReentrantLock源码和AQS源码的一个分析和总结,要说明的是,第8步分析的其实不够完整,因为自己还没有理解透彻,只是将自己理解的部分写了处理,还有一个就是中断操作,这里也没有分析。想要继续了解我没仔细分析部分的可以看看这个大佬的这篇博客https://blog.csdn.net/v123411739/article/details/79304758
如有错误的地方,还请指正