介绍
AQS全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
锁类型
在AQS中的锁类型有两种:分别是Exclusive(独占锁)和Share(共享锁)。
Exclusive 独占
每次只能有一个线程持有锁。例如:ReentrantLock。
Share 共享
允许多个线程同时获取锁,并发访问共享资源。比如ReentrantReadWriteLock
内部实现
AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的线程。
另外**「非公平锁」实现中,新加入的线程节点会「自旋」**尝试的获取锁
FIFO双向队列
FIFO双向队列是一个FIFO的双向链表,这种数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。每个Node其实是由线程封装,当线程争抢锁失败后会封装成Node加入到ASQ队列中去。
在FIFO队列中,「头节点占有锁」,也就是头节点才是锁的持有者,尾指针指向队列的最后一个等待线程节点,除了头节点和尾节点,节点之间都有**「前驱指针」和「后继指针」**
在AQS中维护了一个**「共享变量state」**,标识当前的资源是否被线程持有,多线程竞争的时候,会去判断state是否为0,尝试的去把state修改为1。
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev; //前驱节点
volatile Node next; //后继节点
volatile Thread thread;//当前线程
Node nextWaiter; //存储在condition队列中的后继节点
//是否为共享锁
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
//将线程构造成一个Node,添加到等待队列
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//这个方法会在Condition队列使用,后续单独写一篇文章分析condition
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
释放锁以及添加线程对于队列的变化
添加节点
当出现锁竞争时:
- 新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
- 通过CAS重新指向新的尾部节点
基于ReentrantLock源码分析:
acquire(1)的实现主要有这三步的逻辑:
tryAcquire(arg)
:尝试再次获取锁。分析tryAcquire(arg)
的实现也就是判断state的值是否已经被释放,「若释放则当前线程就会CAS操作将state设置为1,若是没有释放,就会判断是否可以进行锁的重入」。
addWaiter(Node.EXCLUSIVE)
:若是获取锁失败,就会将当前线程组装成一个Node节点,进行入队操作。分析源码,对于新加入的线程添加到双向链表中使用尾插法。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
:acquireQueued方法以addWaiter返回的头节点作为参数,内部实现进行锁自旋,以及判断是否应该执行线程挂起。 从源码中可以看出,只有**「头节点才是锁的持有者」,head的指向会随时改变。会把「原来的头节点进行出队操作,也就是把原来的头节点next指针指向null,利于GC的回收,原来第二节点的prev指针指向null」**。
释放锁移除节点
**head节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,**如果后继节点获得锁成功,会把自己设置为头结点。设置head节点不需要用CAS。
查看源码,state共享变量使用**「volatile关键字」进行修饰,从而保证了可见性。而setState
方法通常。用于当前正持有锁的线程对「state共享变量」**进行修改,因为不存在竞争,是线程安全的,所以没必要使用CAS操作。
- 修改head节点指向下一个获得锁的节点
- 新的获得锁的节点,将prev的指针指向null