AQS源码分析
通过ReentrantLock来解读AQS源码
//JDK源码
private static volatile int i = 0;
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
lock.lock();
i++;
lock.unlock();
}
首先给lock()方法处打断点,然后debug运行程序;
//JDK源码
public class ReentrantLock implements Lock, java.io.Serializable {
public void lock() {
sync.lock();
}
final void lock() {
// 尝试上锁
if (compareAndSetState(0, 1))
// 设置当前线程为独一无二拥有这把锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
在lock()方法里面,我们可以读到它调用了sync.lock(),这里是非公平锁的,一上来直接竞争,用到了CAS(compareAndSetState)操作,尝试0改成1,看看能不能上锁成功,上锁成功就设置当前线程为独一无二拥有这把锁的线程,否则再跟进到acquire(1)里;
//JDK源码
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg){
if(!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
}
acquire(1)方法里面调用了tryAcquire(1)和acquireQueued(addWaiter(Node.EXCLUSIVE),1);
//JDK源码
public class ReentrantLock implements Lock, java.io.Serializable {
static final NonfairSync extends Sync{
protected final boolean tryAcquire(int acquire){
return nonfairTrytAcquire(acquires);
}
}
final boolean nonfairTrytAcquire(int acquire){
// 获取当前线程
final Thread current = Thread.currentThread();
// 拿到AQS核心数值state
int c = getState();
// 如果数值为0说明没人上锁
if(c == 0){
// 给当线程上锁
if(compareAndSetState(0,acquires)){
// 设置当前线程为独一无二拥有这把锁的线程
setExclusiveOwnerThread(current);
return true
}
}
// 判断当前线程是否拥有这个把锁
else if(current == getExclusiveOwnerThread){
// 设置重入
int nextc = c + acquires;
if(nextc < 0)
throw new Error("Maximum lock count wxceeded");
setState(nextc);
return true;
}
return false;
}
}
tryAcquire(1)里面调用了nonfairTrytAcquire(1),大概过程是这样的,首先拿到当前线程,拿到state(AQS)的值,然后进行if判断,如果state的值为0,说明没人上锁,就给自己上锁,当前线程就拿到这把锁,用到了CAS(compareAndSetState)操作,从0让他变成1,state的值设置为1以后,设置当前线程是独一无二的拥有这把锁的线程;否则如果当前线程已经占有这把锁了,我们在原来的基础上加1就可以了,这样就能拿到这把锁了,也就是重入
//JDK源码
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg){
//判断是否得到锁
if(!tryAcquire(arg)
&& acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
// 获取当前要加进来的线程的node(节点)
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;
// CAS操作,把我们这个新节点设置为tail末端
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果设置失败,生成新的节点 or 重新设置
enq(node);
return 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;
}
}
}
}
}
如果tryAcquire(1)拿不到锁,就会调用了acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法,先看看addWaiter()方法,这个方法意思是说你添加等待者的时候,使用的是什么类型(Node.EXCLUSIVE=排他锁,Node.SHARED=共享锁),首先是获得当前要加进等待者队列的线程的节点,把这个新节点的前置节点设置在等待队列的末端,使用CAS(compareAndSetState)操作,把这个新节点设置到tail末端,如果设置失败,进入一个死循环,直到插入节点到队列成功为止,大概过程就是要把当前要加进等待者队列的线程的节点加到等待队列的末端;
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()这个方法,这个方法大概过程,在队列里尝试去获得锁,在队列里排队获得锁,那么它是怎么做到的呢?
首先在for循环里获得了Node节点的前置节点,如果判断前置节点是头节点,就调用tryAcquire(arg)方法尝试一下去得到这把锁,获得了锁后,这个时候说明前置节点释放了,当前节点拿到了这把锁,拿到后设置当前节点为前置节点;
如果没有拿到这把锁,当前节点就会阻塞等着,等着什么?等着前置节点叫醒你,所以它上来之后是竞争,怎么竞争呢?如果你是后面的节点,就老老实实等着,如果你的前面已经是头节点了,说明快轮到我了,那我就试试看能不能拿到这把锁,说不定前置节点这会已经释放这把锁了,如果拿不到就阻塞,等着前置节点释放这把锁以后,叫醒队列里的线程。
public class ReentrantLock implements Lock, java.io.Serializable {
public void unlock() {
sync.release(1);
}
}
在unlock方法里面,我们可以读到它调用了sync.release()方法
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
public class ReentrantLock implements Lock, java.io.Serializable {
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
我们看到release()方法里面主要是调用了tryRelease()方法,大概过程就是看当前线程是否持有锁,不是持有锁的线程,抛异常,然后就看如果state等于0了,表示可以释放锁了,否则state–。
释放锁后,就是上面acquireQueued()方法的逻辑处理了
AQS的核心是一个state,它的下面跟着一个队列(双向链表),这个队列是AQS自己内部所维护的队列,这个队列里边维护的都是一个node节点,它在哪里呢?他在AQS这个类里,属于AQS的内部类,这个节点里边装的是线程,那个线程得到了state这把锁,其它线程都要进入这个队列里边等待,当我们其中一个node得到了state这把锁,就说明这个node里的线程持有这把锁,所以当我们acquire(1)上来以后,看到这个state的值是0,那我就直接拿到state这把锁,现在是非公平上来就抢,抢不着就进队列里acquireQueued(),怎么是抢到呢?先得到当前线程,然后获取state的值,如果state的值等于0,用compareAndSetState(0,acquire)方法尝试把state的值改为1,假如改成功,通过setExclusiveOwnerThread()把当前线程设置为独占statie这个把锁的状态,说明我已经得到这把锁,而且这把锁是互斥的,我得到以后,别人是得不到的,因为别人再来的时候这个state的值已经变成1了,如果说当前线程已经是独占state这把锁了,就往后加个1就表示可重入了。
通过AQS是如何设置链表尾巴的来理解AQS为什么效率这么高
我们的思路是什么呢?假如你要往一个链表上添加尾巴,尤其是好多线程都要往链表上添加尾巴,我们仔细想想看用普通的方法怎么做?
加锁这一点是肯定的,因为多线程,你要保证线程安全,一般的情况下,我们会锁定整个链表(Sync),我们的新线程来了以后,只需要加到尾巴上,但是我们锁定整个链表的话,锁的太多太大了;
AQS用的并不是锁定整个链表的方法,而只是观测tail这一个节点就可以了,怎么做到的呢?
compareAnSetTail(oldTail,node)中oldTail是它的预期值,假如说我们想把当前线程设置为整个链表尾巴的过程中,另外一个线程来了,它插入了一个节点,那么仔细想一下Node oldTail = tail;的整个oldTail还等于整个新的tail吗?不等于了吧,那么既然不等于了,说明中间有线程被其它线程打断了,那如果说却是还是等于原来的oldTail,这个时候就说明没有线程被打断,那我们就接着设置尾巴,只要设置成功了就OK,compareAndSetTail(oldTail,node)方法中的参数node就做为新的tail了,所以用了CAS操作就不需要把原来的整个链表上锁,这也是AQS在效率上比较高的核心。
用CAS(compareAndSet)去操作head和tail,用CAS操作代替了锁整条双向链表的操作
为什么是双向链表?
其实你要添加一个线程节点的时候,需要看一下前面这个节点的状态,如果前面的节点是持有线程的过程中,这个时候你就得在后面等着,如果说前面这个节点已经取消掉了,那你就应该越过这个节点,不去考虑它的状态,所以你需要看前面节点状态的时候,就必须是双向的。
lock方法//JDK源码public class ReentrantLock implements Lock, java.io.Serializable {public void lock() { sync.lock(); }final void lock() {// 尝试上锁 if (compareAndSetState(0, 1)) // 设置当前线程为独一无二拥有这把锁的线程 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);}}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update);}}1234567891011121314151617181920212223在lock()方法里面,我们可以读到它调用了sync.lock(),这里是非公平锁的,一上来直接竞争,用到了CAS(compareAndSetState)操作,尝试0改成1,看看能不能上锁成功,上锁成功就设置当前线程为独一无二拥有这把锁的线程,否则再跟进到acquire(1)里;
acquire()//JDK源码public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg){if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) selfInterrupt();}}1234567891011acquire(1)方法里面调用了tryAcquire(1)和acquireQueued(addWaiter(Node.EXCLUSIVE),1);
tryAcquire()//JDK源码public class ReentrantLock implements Lock, java.io.Serializable { static final NonfairSync extends Sync{ protected final boolean tryAcquire(int acquire){ return nonfairTrytAcquire(acquires); } }
final boolean nonfairTrytAcquire(int acquire){ // 获取当前线程final Thread current = Thread.currentThread(); // 拿到AQS核心数值state int c = getState(); // 如果数值为0说明没人上锁 if(c == 0){ // 给当线程上锁if(compareAndSetState(0,acquires)){ // 设置当前线程为独一无二拥有这把锁的线程 setExclusiveOwnerThread(current); return true } } // 判断当前线程是否拥有这个把锁 else if(current == getExclusiveOwnerThread){ // 设置重入int nextc = c + acquires; if(nextc < 0) throw new Error("Maximum lock count wxceeded"); setState(nextc); return true; } return false; }}1234567891011121314151617181920212223242526272829303132333435tryAcquire(1)里面调用了nonfairTrytAcquire(1),大概过程是这样的,首先拿到当前线程,拿到state(AQS)的值,然后进行if判断,如果state的值为0,说明没人上锁,就给自己上锁,当前线程就拿到这把锁,用到了CAS(compareAndSetState)操作,从0让他变成1,state的值设置为1以后,设置当前线程是独一无二的拥有这把锁的线程;否则如果当前线程已经占有这把锁了,我们在原来的基础上加1就可以了,这样就能拿到这把锁了,也就是重入
acquireQueued()//JDK源码public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {public final void acquire(int arg){ //判断是否得到锁if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg)) selfInterrupt();}private Node addWaiter(Node mode) {// 获取当前要加进来的线程的node(节点) 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; // CAS操作,把我们这个新节点设置为tail末端 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果设置失败,生成新的节点 or 重新设置 enq(node); return 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; } } }}}12345678910111213141516171819202122232425262728293031323334353637383940414243444546如果tryAcquire(1)拿不到锁,就会调用了acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法,先看看addWaiter()方法,这个方法意思是说你添加等待者的时候,使用的是什么类型(Node.EXCLUSIVE=排他锁,Node.SHARED=共享锁),首先是获得当前要加进等待者队列的线程的节点,把这个新节点的前置节点设置在等待队列的末端,使用CAS(compareAndSetState)操作,把这个新节点设置到tail末端,如果设置失败,进入一个死循环,直到插入节点到队列成功为止,大概过程就是要把当前要加进等待者队列的线程的节点加到等待队列的末端;
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); }}1234567891011121314151617181920212223接下来我们来解读acquireQueued()这个方法,这个方法大概过程,在队列里尝试去获得锁,在队列里排队获得锁,那么它是怎么做到的呢?首先在for循环里获得了Node节点的前置节点,如果判断前置节点是头节点,就调用tryAcquire(arg)方法尝试一下去得到这把锁,获得了锁后,这个时候说明前置节点释放了,当前节点拿到了这把锁,拿到后设置当前节点为前置节点;如果没有拿到这把锁,当前节点就会阻塞等着,等着什么?等着前置节点叫醒你,所以它上来之后是竞争,怎么竞争呢?如果你是后面的节点,就老老实实等着,如果你的前面已经是头节点了,说明快轮到我了,那我就试试看能不能拿到这把锁,说不定前置节点这会已经释放这把锁了,如果拿不到就阻塞,等着前置节点释放这把锁以后,叫醒队列里的线程。
unlock方法public class ReentrantLock implements Lock, java.io.Serializable {public void unlock() { sync.release(1);}}12345在unlock方法里面,我们可以读到它调用了sync.release()方法
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false;}}
public class ReentrantLock implements Lock, java.io.Serializable {protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free;}}12345678910111213141516171819202122232425262728我们看到release()方法里面主要是调用了tryRelease()方法,大概过程就是看当前线程是否持有锁,不是持有锁的线程,抛异常,然后就看如果state等于0了,表示可以释放锁了,否则state–。释放锁后,就是上面acquireQueued()方法的逻辑处理了
总结AQS的核心是一个state,它的下面跟着一个队列(双向链表),这个队列是AQS自己内部所维护的队列,这个队列里边维护的都是一个node节点,它在哪里呢?他在AQS这个类里,属于AQS的内部类,这个节点里边装的是线程,那个线程得到了state这把锁,其它线程都要进入这个队列里边等待,当我们其中一个node得到了state这把锁,就说明这个node里的线程持有这把锁,所以当我们acquire(1)上来以后,看到这个state的值是0,那我就直接拿到state这把锁,现在是非公平上来就抢,抢不着就进队列里acquireQueued(),怎么是抢到呢?先得到当前线程,然后获取state的值,如果state的值等于0,用compareAndSetState(0,acquire)方法尝试把state的值改为1,假如改成功,通过setExclusiveOwnerThread()把当前线程设置为独占statie这个把锁的状态,说明我已经得到这把锁,而且这把锁是互斥的,我得到以后,别人是得不到的,因为别人再来的时候这个state的值已经变成1了,如果说当前线程已经是独占state这把锁了,就往后加个1就表示可重入了。
通过AQS是如何设置链表尾巴的来理解AQS为什么效率这么高我们的思路是什么呢?假如你要往一个链表上添加尾巴,尤其是好多线程都要往链表上添加尾巴,我们仔细想想看用普通的方法怎么做?加锁这一点是肯定的,因为多线程,你要保证线程安全,一般的情况下,我们会锁定整个链表(Sync),我们的新线程来了以后,只需要加到尾巴上,但是我们锁定整个链表的话,锁的太多太大了;
AQS用的并不是锁定整个链表的方法,而只是观测tail这一个节点就可以了,怎么做到的呢?compareAnSetTail(oldTail,node)中oldTail是它的预期值,假如说我们想把当前线程设置为整个链表尾巴的过程中,另外一个线程来了,它插入了一个节点,那么仔细想一下Node oldTail = tail;的整个oldTail还等于整个新的tail吗?不等于了吧,那么既然不等于了,说明中间有线程被其它线程打断了,那如果说却是还是等于原来的oldTail,这个时候就说明没有线程被打断,那我们就接着设置尾巴,只要设置成功了就OK,compareAndSetTail(oldTail,node)方法中的参数node就做为新的tail了,所以用了CAS操作就不需要把原来的整个链表上锁,这也是AQS在效率上比较高的核心。
用CAS(compareAndSet)去操作head和tail,用CAS操作代替了锁整条双向链表的操作
为什么是双向链表?其实你要添加一个线程节点的时候,需要看一下前面这个节点的状态,如果前面的节点是持有线程的过程中,这个时候你就得在后面等着,如果说前面这个节点已经取消掉了,那你就应该越过这个节点,不去考虑它的状态,所以你需要看前面节点状态的时候,就必须是双向的。————————————————版权声明:本文为CSDN博主「程序员Forlan」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_36433289/article/details/125666450