目录
AQS是什么
当我们构造一个ReentrantLock对象的时候,可以通过传入一个布尔值来指定公平锁还是非公平锁,进入源码可以发现区别是new了不同的sync对象
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
点进去FairSync或者NonfairSync的源码可以发现,其实它们都间接继承了AbstractQueuedSynchronizer类,该抽象类为我们的加锁解锁过程提供了模板方法,所以我们先来了解下它
AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。
AQS的内部主要是构造了一个先进先出的双向队列,把抢锁失败的线程放入队列中并阻塞该线程,等锁释放后再唤醒线程.我们先来看看AQS的内部结构(只展示ReentrantLock中用到的)
//头结点
private transient volatile Node head;
//尾巴节点
private transient volatile Node tail;
//状态变量 加锁就是对这个变量CAS
private volatile int state;
static final class Node {
//节点等待状态
volatile int waitStatus;
//上一个节点
volatile Node prev;
//下一个节点
volatile Node next;
//节点的线程
volatile Thread thread;
Node nextWaiter;
}
明白了AQS的基本结构后,其实我们就可以先大概猜测一下
加锁: 对state变量进行CAS,失败的话则新建一个node对象,把自己放到head的后面,然后此时又拿不到锁,没啥事做,那就自闭吧,把线程阻塞起来,防止cpu空转.
解锁: 同样对state变量进行CAS,然后再通知其他线程来抢锁,最重要的是要把之前阻塞的线程唤醒,然后有个线程重新获得锁,周而复始.
ok,进入源码验证下我们的猜测吧
加锁源码
ReentrantLock一般是使用非公平锁,所以我们先看看非公平锁的源码
final void lock() {
//直接CAS加锁看能否成功
if (compareAndSetState(0, 1))
//成功后设置当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
上面直接先尝试CAS,如果成功后把占用锁的线程设置成自己,加锁失败则进入acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//补偿中断状态
selfInterrupt();
}
tryAcquire方法作用
首先来看看tryAcquire方法里面做了什么,点进去会发现最终的实现是nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁状态
int c = getState();
if (c == 0) {//直接尝试加锁,这里和外层的尝试加锁是一样的,只是再尝试一次
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
//加锁成功的话就直接返回true了,外层的acquire方法其实就直接结束了
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;
}
//返回false说明需要放入队列
return false;
}
可以发现tryAcquire做了两件事: 1,再次尝试加锁 2,持有锁的是否是自己 如果return
true的话,方法就直接结束了,如果reture
false的话,则会进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
addWaiter方法
那我们先来看下addWaiter方法
private Node addWaiter(Node mode) {
//此时mode为null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//tail是结尾的指针,赋值给pred,当第一个线程进来的时候,tail为null
Node pred = tail;
//此处判断pred是否为null就是判断tail是否为null,也就是判断队尾是否有节点
//如果不为空,直接把当前节点放到tail的后面
if (pred != null) {
//把当前节点放在tail后面
node.prev = pred;
//把当前节点设置为tail指针,其实就是把当前节点设置成最后一个
if (compareAndSetTail(pred, node)) {//CAS确保入队时是原子操作
//当前node成为新的队尾
pred.next = node;
return node;
}
}
//如果为空,执行下面的方法
enq(node);
return node;
}
private Node enq(final Node node) {
//死循环初始化队列
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//死循环的第一次会初始化tail和head,刚开始的时候其实tail和head是一样的
if (compareAndSetHead(new Node()))
tail = head;
} else {
//这里的代码和addWaiter中的是一样的
node.prev = t;
//死循环的第二次会把当前node接到之前的tail位置后面
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
可以发现addWaiter方法的作用其实就是把当前node设置到tail节点的后面,如果tail节点为空的话则执行enq方法初始化head和tail节点,无论结果如何,addWaiter都会返回当前的node
acquireQueued方法
然后下一步进入acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
//标志1
boolean failed = true;
try {
//标志2
boolean interrupted = false;
//死循环
for (;;) {
//找到当前node的prev节点
final Node p = node.predecessor();
//我们可以把head当做当前持有锁的节点,如果前一个是head,则开始重新获取锁
if (p == head && tryAcquire(arg)) {
//如果此时获取锁成功,则把当前节点放到head位置
setHead(node);
//node代替了之前的head,所以把之前的head置为null
p.next = null; // help GC
failed = false;
//加锁成功
return interrupted;
}
//有两种情况会到这
//1,前一个不是head,说明还有其他兄弟在排队
//2,前一个是head, 但是head还没有释放锁,加锁失败了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
有两种情况会去执行shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
1,前一个不是head,说明还有其他兄弟在排队
2,前一个是head, 但是head还没有释放锁,加锁失败了
通过方法名称可以知道shouldParkAfterFailedAcquire是检测当前线程是否有挂起的资格
parkAndCheckInterrupt则是说明在shouldParkAfterFailedAcquire返回true的情况下,挂起线程
shouldParkAfterFailedAcquire方法
首先来看下shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//上一个节点的状态
int ws = pred.waitStatus;
//如果是SIGNAL,直接返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
//如果是CANCELLED状态,循环往队列前面找,直到找到一个SIGNAL的节点,然后把当前节点放到SIGNAL节点的后面
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//把之前的节点设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
可以发现shouldParkAfterFailedAcquire方法的作用就是保证当前node节点的prev节点的状态必须是SIGNAL,当满足这个条件后才会去执行parkAndCheckInterrupt方法,否则就会进入下一次死循环直到保证
prev节点的状态是SIGNAL,因为只有当prev节点是SIGNAL状态时,后续才会去唤醒下一个节点,当前节点才敢把自己挂起,
parkAndCheckInterrupt方法
然后我们看下parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
//阻塞当前线程
LockSupport.park(this);
//这里返回true后会清除掉状态
return Thread.interrupted();
}
这里就是直接挂起自己,等待head节点把自己唤醒,到这里的话,非公平锁的加锁过程就结束了
可以看到和我们之前的猜测基本一致,非公平锁的实现我们已经了解了,那么公平锁的实现和非公平锁有啥区别呢
公平锁是怎么实现公平的呢?
我们看看公平锁和非公平锁两者源码的区别
区别1
//公平锁
final void lock() {
acquire(1);
}
//非公平
final void lock() {
//直接CAS加锁看能否成功
if (compareAndSetState(0, 1))
//成功后设置当前线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
上面我们对比了lock方法 如果是非公平锁,新进来一个线程会直接去尝试加锁,根本不会排队 公平锁则直接进入了acquire方法
区别2
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
上面是公平锁的tryAcquire方法,我们可以看到这个和非公平锁的区别只是多了一个hasQueuedPredecessors()判断方法,同样,我们先看看这个方法是干啥的
hasQueuedPredecessors方法的作用
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
//判断队列中是否有优先级更高的等待线程
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//这里如果返回false 才会去CAS抢锁,否则就去排队
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这个方法是判断队列中是否有优先级更高的等待线程 返回true:有优先级更高的等待线程,当前线程乖乖去排队
返回false:没有优先级更高的线程, 直接去CAS抢锁
然后我们来分析下
h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
h != t有哪些情况?
- 情况1: head为null, tail不为null
- 情况2: head不为null, tail为null
- 情况3: head和tail都不为null且不相等
从enq方法可以看到head是先于tail设置的,所以情况1是不存在的
然后来到下一步
((s = h.next) == null || s.thread != Thread.currentThread());
情况2: 这时其他线程进入enq方法刚执行完compareAndSetHead(new Node()),但是还没有给tail赋值 此时(s= h.next) == null 为 true,直接退出循环,此时说明有优先级更高的线程在执行任务
情况3: 队列已经初始化成功, 此时(s= h.next) == null 为 false, 然后判断当前节点是不是head节点的下一个节点,请注意之前已经给s = h.next赋值了,如果 s.thread != Thread.currentThread()说明当前节点也不是第二个节点,那么就退出循环,乖乖排队去
发现没,(s = h.next) == null这个判断其实就是区分上面的情况1和情况2的,只是过于简洁导致不太好看懂
总结一下,实现公平锁就是通过hasQueuedPredecessors方法来判断是否有高优先级的线程,而不是像非公平锁一样直接去抢锁.
提示下大家, 无论非公平锁还是公平锁,都是在线程没入队列之前操作的, 但是只要入了队列就必须乖乖排队
总结一下加锁流程
线程尝试加锁,加锁失败后会进入AQS队列,队列第一次会初始化一个head节点,此时head节点的内部线程是null,然后第一个加入队列的线程节点thread1会接在初始化后的head节点后面,然后thread1会park自己,也就是说,除了head节点外,队列中其他的节点都会park自己,然后持有锁的线程释放锁后,会唤醒head节点后面的第一个节点,此时也就是thread1会被唤醒,thread获取到锁后会把自己置为head,并且把自身的thread置为null,因为拿到锁后会setExclusiveOwnerThread(current);所以没有必要再持有线程.
除了第一次初始化的head外,所有的head节点都是已经拿到锁的节点
解锁源码
解锁的话相对来说就比较简单了
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法的流程基本就是对state做操作,然后如果队列有下一个节点,则去唤醒下一个节点
tryRelease方法
先看下tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//拥有锁的线程才能解锁
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c == 0才算解锁成功, 也就是说加几次锁必须解几次锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor唤醒线程
再看下是怎么唤醒下一个线程的
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
//这里的状态有 0:初始化状态 -1 :就是之前的SIGNAL状态 1:CANCELLED状态
int ws = node.waitStatus;
if (ws < 0)
//把当前状态置为初始状态0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
//从队列最后往前遍历,找到离head节点最近的状态为SIGNAL的节点,然后唤醒
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果不为null,直接唤醒下一个线程,下一个线程会把自己重新设置成head
if (s != null)
LockSupport.unpark(s.thread);
}
为啥从后往前遍历队列?
这里有个问题,为啥这里的队列要从后往前遍历呢?
既然这么写,那肯定是因为如果从前往后遍历会出现某种问题,那么会有啥问题呢?
我们假设有这样一种场景
现在有A,B,C三个线程在执行任务
线程A当前已经排在了任务队列的最后面,也就是tail节点
1,此时线程B执行了入队的enq方法中的compareAndSetTail(t, node),但是还没有执行t.next=node的时候,
2,线程C执行了compareAndSetTail(t, node)并且执行了t.next = node;这时候会怎么样?
当线程B所在的节点入队后,但是还没有执行t.next=node的时候,队列是这样的
此时已经把tail设置给了线程B所在的node2节点,但是此时还没有设置node1的next节点
此时cpu的时间片分给了线程C,线程C开始执行入队任务并且执行了t.next = node,此时的队列是这样的
发现了没,这个时候node1节点的next还是为null,所以在这种情况下从前往后遍历的话,队列就断了,
这里可以看到每个节点的prev都是能找到节点的,所以这里要从后往前遍历
结尾
有啥理解不当之处希望大家能指出来,有问题的话欢迎一起讨论~