定义
- 全称是 AbstractQueuedSynchronizer
- 阻塞式和相关的同步器工具框架
- AQS用一个变量 (volatile state) 属性来表示锁的状态,子类去维护这个状态
- getState、compareAndSetState CAS改变这个变量
- 独占模式是只有一个线程能够访问资源
- 而分享模式可以允许多个线程访问资源(读写锁)
- 内部维护了一个FIFO等待队列,类似 synchronized 关键字当中的 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似 Monitor 的 WaitSet
- 内部维护了一个 Thread exclusiveOwnerThread 来记录当前持有锁的那个线程
功能
- 实现阻塞获取锁 acquire 拿不到锁就去阻塞 等待锁被释放再次获得锁
- 实现非阻塞尝试获取锁 tryAcquire 拿不到锁则直接放弃
- 实现获取锁超时机制
- 实现通过打断来取消
- 实现独占锁及共享锁
- 实现条件不满足的时候等待
AQS关键设计
private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
private transient Thread exclusiveOwnerThread;//持有锁的线程
Node的关键设计
这里的node可以看成是一个线程的包装类,简而言之一个node对象代表一个线程
public class Node{
volatile Node prev; //上一个节点
volatile Node next; //下一个节点
volatile Thread thread; //当前node包含的线程对象
volatile int waitStatus;
}
waitStatus
waitStatus >0 代表取消的节点
只有上一个节点是SIGNAL,当前节点才有可能被上一个节点唤醒
// CANCELLED:由于超时或中断,此节点被取消。
节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。
static final int CANCELLED = 1;
// SIGNAL:此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点
// 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
static final int SIGNAL = -1;
// 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),
直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
static final int CONDITION = -2;
//传播:应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点)
,以确保传播继续,即使此后有其他操作介入。
static final int PROPAGATE = -3;
//0:以上数值均未按数字排列以简化使用。非负值表示节点不需要发出信号。
所以,大多数代码不需要检查特定的值,只需要检查符号。
//对于正常同步节点,该字段初始化为0;对于条件节点,该字段初始化为条件。
它是使用CAS修改的(或者在可能的情况下,使用无条件的volatile写入)。
ReentrantLock和AQS的关系
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
ReentrantLock加锁过程
第一个线程t1 没有加锁之前 ReentrantLock被实例化之后 队列的状态
lock方法的过程(分为公平和非公平)
- 获取当前线程 final Thread current = Thread.currentThread();
- 获取锁的状态 getState(); int c = getState();
- 判断锁的状态 if (c == 0)
- 如果锁是自由状态则第5步,如果不是自由状态则第7步
- 公平锁需要判断自己是否需要排队,非公平锁不需要
什么情况下当前线程不需要排队(排队=入队+阻塞)
- 队列没有初始 对头和队尾等于null的时候是不需要排队
- 队列当中只有一个人的时候是不需要排队的
- 如果不需要排队——则cas加锁 成功则直接返回;加锁流程结束,执行临界区的代码
- 判断是否重入(一般情况下不重入)如果第6步执行,则没有这一步;这是第4步的分支
- 直接返回false(加锁失败,不考虑重入)
- 加锁失败之后会调用addWaiter,主要是入队(入队不等于排队)
入队完成之后第一个节点是一个虚拟出来的thread等于null的节点,而不是我们入队的节点?
为什么这里需要虚拟出来一个节点,而不是拿当前节点作为头部?
- 判断是否需要自旋
自旋的条件也很简单,判断自己的上一个节点是否头节点
- 如果不需要自选则直接park
- 如果需要自旋,则再次尝试加锁,失败则park,成功则维护好队列然后返回执行临界区代码
第一种情况
第一个线程t0获取锁的时候 代价基本为0(cas改变锁的状态,继而记录当前持有锁的线程);队列的状态如下图
t0加锁成功之后
当t0释放锁之后;队列的状态如下图
第二种情况
当第一个线程t0释放锁之后第二个线程t1来加锁(t0 和t1是没有竞争执行),代价基本为0,(cas改变锁的状态,继而记录当前持有锁的线程)
当前状态 这种情况下t1加锁成功后队列状态如下图
如果t1释放锁则如下图
总结:如果是线程交替执行(没有竞争) 不管并发量多大 性能都是非常高的,因为仅仅是CAS操作,队列压根就没有
第三种情况
t0没有释放锁这个t1来加锁;因为t0没有释放锁故而队列的情况如下图(注意这个时候是t1来加锁还未成功的时候的状态)
t1会加加锁失败;返回false,然后调用addWaiter方法区初始化队列,然后自己入队
- 把t1封装成为一个node Node node = new Node(Thread.currentThread(), mode);
调用ENQ方法 enq(node);
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;
}
}
}
}
设置一个空的节点为尾部节点 if (compareAndSetHead(new Node()))
- 让头部节点和尾部节点都指向这个new出来的空节点 tail = head;
- 设置t1-node的上一个节点=new node;node.prev = t;
- 设置队列的尾部为t1-node if (compareAndSetTail(t, node))
- 设置new-node的下一个节点为t1-node t.next = node;
到此为止 addWaiter方法执行完 t1-node 入队
入队成功之后调用acquireQueued
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);
}
}
acquireQueued 首先获取t1-node的上一个节点;final Node p = node.predecessor();获取上一个节点
- 判断上一个节点是否头节点
- 如果是头结点则再次获取锁(一次自旋) if (p == head && tryAcquire(arg))
- 如果能够获取到锁表示锁被释放了
- setHead(node);//设置t1-node(当前加锁的线程)为头节点
- node.thread = null; //t1-node(当前node)的thread=null
- node.prev = null;//t1-node(当前node)的上一个节点=null
- p.next = null; // help GC 把new-node(上一个节点)的下一个节点指向null,为了gc
- t0没有释放锁,t2自旋之后还是没有获取到锁则park 排队
第四种情况
t2来加锁,t0没有释放锁,t1这个时候已经排队(和第三种情况的区别在于t2入队之后不会自旋,直接排队);为什么不需要自旋?
t2加锁成功之后状态如下图
RenntrantLock解锁流程
第一种情况
只有一个t0加锁了
当调用unlock解锁的时候
- sync.release(1);
- tryRelease
int c = getState() - releases; //把锁的状态改为自由状态
- if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();
这种情况一般不会出现,就是解锁的线程和持有的线程不一样,则会出异常 除非你没有加锁直接解锁则会异常
- boolean free = false; 只是标识一下目前还没有释放锁成功,因为你仅仅把锁改成了自由状态,线程没有释放(而且还有可能是重入),所以这个变量是一个过渡变量
- setExclusiveOwnerThread(null) 把持有锁的线程改为null 锁彻底释放了
- 返回true
- 以上是释放锁,接下来可能需要唤醒队列当中的阻塞线程去获取锁(也有可能不需要唤醒)
- Node h = head;//拿到对头
- if (h != null && h.waitStatus != 0);//判断是否有对头(是否有人在排队)
- 由于当前这种情况(只有t0线程存在)肯定没有人排队则不需要唤醒,则return true 标识解锁成功
第二种情况
就是队列当中有人排队(不考虑有几个人)
- 调用tryRelease方法释放
- if (h != null && h.waitStatus != 0) 判断是否需要唤醒下一个,当前这种情况肯定需要唤醒一个
- unparkSuccessor(h); 这个h对象表示的是头结点,因为Node h = head;把头结点传给unparkSuccessor
- compareAndSetWaitStatus(node, ws, 0); 把头结点的ws改为0(因为头结点的ws是-1,这里需要把它为0)
- Node s = node.next;得到队列当中第一个排队的人 也就是图上的t1所标识的node对象
- LockSupport.unpark(s.thread); //唤醒t1线程
- 由于t1在lock方法中被阻塞那么唤醒则也是从lock方法中被唤醒往下执行
- Thread.interrupted()这个方法主要干嘛?清除打断标记
private final boolean parkAndCheckInterrupt() {
//简单的唤醒t2
LockSupport.park(this);
//这里为什么需要调用一下Thread.interrupted()
return Thread.interrupted();
}