1.什么是AQS?
AQS,指的是一个抽象类,全名为 java.util.concurrent.locks.AbstractQueuedSynchronizer.它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS的基本数据结构:
它维护了一个state字段,表示当前资源的状态(0表示未被持有,1表示被线程持有,1以上表示重入的次数)
和一个FIFO的队列
以ReentrantLock为例子,对AQS的源码进行分析
2.ReentrantLock
ReentrantLock是JUC包下的一个同步工具类,基于AQS实现。
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
...
}
可以看到,它实现了lock接口,并且包含一个Sync的实例对象.该对象继承了AbstractQueuedSynchronizer类,并提供了两种实现
1.NonfairSync 非公平锁
2.FairSync 公平锁
以公平锁为例,阅读ReentrantLock 源码
首先从构造方法开始
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
//无参构造方法,默认把sync初始化为非公平锁
sync =new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
//含参构造方法,true的时候将sync初始化为公平锁,否则是非公平锁
sync = fair ?new FairSync() :new NonfairSync();
}
lock方法(翻译了源码的注释):
如果锁不是由另一个线程持有,则获取该锁并返回*立即,将锁定保持计数设置为1。
如果当前线程已经持有锁,则*count递增1,方法立即返回。
如果锁由另一个线程持有,则*当前线程在线程调度中被禁用*目的和休眠直到锁被获得,此时锁定保持计数设置为1。
public void lock() {
sync.lock();
}
可以看到,调用了sync的lock方法。因为Sync是一个抽象类,要看具体实现.在IDEA中,Crtl+alt+B 可以跟踪实现类,进入FairSync查看该方法。
下面是该类的实现:
可以看到,sync的lock方法调用了acquire(1)这个方法。该方法是AQS类的方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg) 是尝试加锁的方法,尝试成功,返回true
当尝试加锁失败时,就需要调用acquireQueued()方法,把自己加入线程队列中,然后Park()
下面分析公平锁FairSync的tryAcquire()方法:
1.首先获取当前线程、
2.获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
3.当锁是自由状态时,调用hasQueuedPredecessors方法,判断自己是否需要排队。(因为是公平锁,要保证来的早的线程可以先拿到锁,所以在进来之前,要先判断一下是否需要排队。非公平锁会在这里会直接上锁,不平判断是否需要排队。)
如果不需要排队,则尝试CAS加锁。加锁成功,设置当前线程为排它的拥有者,返回true,lock方法执行完成。
4.如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
5.如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
当这个方法尝试加锁失败,会返回acquire,这里有取反,所以条件成立,执行&&后面的逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时程序来到了addWaiter(Node.EXCLUSIVE)方法。
Node.EXCLUSIVE = null;是AQS类的一个工具变量
这里的Node是AQS队列的链表节点,因为是双向链表,所以要有前后指针。值放的就是Thread对象。
addWaiter方法的作用,就是往链表添加一个等待节点。做的操作如下:
1.当前线程装入节点
2.队尾指向一个临时变量
3.判断队尾是否为空
判断pred是否为空,其实就是判断对尾是否有节点,其实只要队列被初始化了对尾肯定不为空。假设队列里面只有一个元素,那么对尾和对首都是这个元素,其实这里就是判断队列有没有初始化.
不为空时,代表队列已经初始化。先将当前节点T1的前驱指向队尾,然后通过CAS操作(竞争激烈时确保原子操作),把队尾的后继替换成自己,返回出去。
队尾为空时,意味着队列没有初始化。调用enq方法,进行初始化。
enq方法:
第一次循环,队尾是null
那么,使用CAS,把链表的头部设置成一个空节点,并且让队尾等于队首。此时,队列里就初始化出来了一个空节点。
进入第二次循环
队尾现在是一个空对象,但不是null。将当前的节点的前驱设为队尾,然后CAS把队尾替换成自己,返回。
然后就进入了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
第一次循环,获取当前线程所在节点的上一个节点.
1.如果上一个节点是head,表示自己是第一个排队的,那么此时,有可能资源已经被释放了,所以再次尝试加锁。
2.尝试成功的话,表示该线程得到锁,然后把自己修改成头结点(setHead方法里的操作,是把头结点设置成自己,然后把该节点的前驱设为空,内容设为空.由此可见,队列中的头结点,永远是空节点)。
3.如果不是第二个节点,或竞争失败,那就应该去排队了。shouldParkAfterFailedAcquire()方法里,是修改waitStatus。他会获取上一个节点的waitStatus,如果是-1,表示上一个节点已经release,无需再操作,直接返回。如果大于0,表示上一个节点已经取消(?这里涉及到unlock,以后再说).如果是==0,就通过CAS操作把上一个节点的waitStatus改成-1,表示上一个节点以已经PARK了。
所以,正常情况下队列中的节点,应该是这样的:
因为上一个节点park后,已经不能继续执行了,所以,是由下一个节点来修改它的状态的。