提升--09---AQS源码解析

Posted 高高for 循环

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--09---AQS源码解析相关的知识,希望对你有一定的参考价值。

文章目录


ReentrantLock—源码解析

以下jdk版本----JDK11

案例debug

import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
    private static volatile int i = 0;

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();

        try {
            i++;
        } finally {
            lock.unlock();
        }

    }

}

UML图

debug开始

ReentrantLock

  • ReentrantLock调用它lock()方法的时候,它会调用acquire(1)方法,谁的acquire?是Sync的acquire,这很容易理解,

NonfairSync

  • 在我们的lock方法里它调用了内部的一个类,这个类叫NonfairSync,NonfairSync它里边有个方法叫acquire(1)

AbstractQueuedSynchronizer----(AQS)

  • NonfairSync的父类是Sync,因为你看到NonfairSync子类里方法的时候,它有可能用到父类的方法,所以你要去父类里读才可以,
  • NonfairSync的父类是Sync,好这个时候再回到Sync,这个时候我们看一下,这个Sync的父类又是谁?是AQS(AbstarctQueuedSynchronizer),这个时候说一下AQS是所有锁的核心,
  • 我们继续lock调用了acquire(1)这个方法,是谁的acquire(1)?是NonfairSync的acquire(1),NonfairSync又是谁?他是Sync的子类,Sync又是谁,Sync是AQS的子类,所以调用了acquire(1)我们再跟进去,这个时候就调用了AQS的acquire(1)了

NonfairSync ==> Sync ==> AQS(AbstractQueuedSynchronizer)


tryAcquire(1)

  • AQS里面调用的是什么呢?是tryAcquire(1),再跟进去你会发现这次调用的是NonfairSync里的tryAcquire(1),刚才我们读的时候已经知道AQS里有一个tryAcquire(1),但是它里面是抛出了一个异常,所以很容易理解,是NonfairSync里边重写了AQS里的tryAcquire(1),
  • 所以AQS里的acquire(1)调用了NonfairSync里的tryAcquire(1),我们再来看NonfairSync里的tryAcquire(1)又调用了nonfairTryAcquire(acquires),

我们再跟进这个时候读到这里,我们就必须要了解AQS了,如果不懂,就没办法继续进行下去了,

nonfairTryAcquire

  • 我们看nonfairTryAcquire(acquires)方法实现,开始它得到了当前线程,跟线程有关系了

  • 这个时候出现了一个方法getState(),我们来看一下这个方法,我们跟进你会发现,这个方法又尽到了AQS类里,这个getState()方法返回了一个state,这个state是什么呢?按着ctrl点过去,你会发现这个state就是一个volatile修饰的int类型的数,这个时候就牵扯到AQS的结构了

AQS----源码解析

AQS队列又可以称为CLH队列

1.state

AQS的核心是什么?就是这个state

注意这个state 是用 volatile修饰

ReentrantLock的state 0和1就代表了加锁和解锁

  • AQS队列又可以称为CLH队列,AQS的核心是什么?就是这个state,这个state所代表的意思随你定,随子类来定,
  • 我们现在讲的是ReentrantLock,刚才state的值是0,当你获得了之后它会变成1,就表示当前线程得到了这把锁什么时候你释放完了,state又会从1变回0,说明当前线程释放了这把锁,所以这个state0和1就代表了加锁和解锁,所以这个state的值是根据你子类不同的实现取不同的意义

2.AQS队列

AQS的核心是一个state,以及监控这个state的双向列表


  • 这个state的值的基础之上,它的下面跟着一个队列,这个队列是AQS自己内部所维护的队列,这个队列里边每一个所维护的都是node一个节点,它在哪里呢?他在AQS这个类里属于AQS的内部类,

Node

  • 在这个node里最重要的一项是他里面保留了一个Thread一个线程,所以这个队列是个线程队列
  • 而且还有两个prev和next分别是前面的节点和后面的节点
  • 所以AQS里边的队列是这样子的,一个一个的node,node里装的是线程Thread,这个node它可以指向前面的这一个,也可以指向后面的这一个,所以叫双向列表,

所以AQS的核心是一个state,以及监控这个state的双向列表,每个列表里面有个节点,这个节点里边装的是线程,那么那个线程得到了state这把锁,其他没抢到锁的线程要等待,都要进入这个队列里边,当我们其中一个node得到了state这把锁,就说明这个node里得线程持有这把锁,

3.获取锁的过程

所以当我们acquire(1)上来以后看到这个state的值是0,那我就直接拿到state这个把锁,现在是非公平上来就抢,抢不着就进队列里acquireQueued(),怎么是抢到呢?

  1. 先得到当前线程,然后获取state的值
  2. 如果state的值等于0,用compareAndSetState(0,acquire)方法尝试把state的值改为1,假如改成了setExclusiveOwnerThread()把当前线程设置为独占statie这个把锁的状态,说明我已经得到这个把锁,而且这个把锁是互斥的,我得到以后,别人是得不到的,因为别人再来的时候这个state的值已经变成1了,
  3. 如果说当前线程已经是独占state这把锁了,就往后加个1就表示可重入了。

4. 什么叫非公平的去获得(nonfairTryAcquire)?

  • 什么叫公平什么叫非公平,当我们获得一把锁的时候,有一些等待队列,如果说新来了一个线程要获得这个锁的时候,先去检查等待队列有没有人,如果有后面排队,这个叫公平,上来二话不说,我才不管你的队列有没有人在等着,我上来就抢,抢着就算我的,插队非公平。

通过ReentrantLock来解读AQS源码

回顾:

AQS大家还记得吗?最核心的是它的一个共享的int类型值叫做state,这个state用来干什么,其实主要是看他的子类是怎么实现的,比如ReentrantLock这个state是用来干什么的?拿这个state来记录这个线程到底重入了多少次,比如说有一个线程拿到state这个把锁了,state的值就从0变成了1,这个线程又重入了一次,state就变成2了,又重入一次就变成3等等,什么时候释放了呢?从3变成2变成1变成0就释放了,

这个就是AQS核心的东西,一个数,这个数代表了什么要看子类怎么去实现它,那么在这个state核心上还会有一堆的线程节点,当然这个节点是node,每个node里面包含一个线程,我们称为线程节点,这么多的线程节点去争用这个state,谁拿到了state,就表示谁得到了这把锁,

AQS得核心就是一个共享的数据,一堆互相抢夺竞争的线程,这个就是AQS

AQS的核心是一个state,以及监控这个state的双向列表

debug运行程序

1.接着上面来讲,首先给lock()方法处打断点,然后debug运行程序

import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
    private static volatile int i = 0;

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        lock.lock();

        try {
            i++;
        } finally {
            lock.unlock();
        }

    }

}

2.在lock()方法里里面,我们可以读到它调用了sync.acquire(1),

//JDK源码
public class ReentrantLock implements Lock, java.io.Serializable {
	public void lock(){
        sync.acquire(1);
    }
}


3.再跟进到acquire(1)里,可以看到acquire(1)里又调用了我们自己定义自己写的那个tryAcquire(arg)

//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();
	}
}


4.跟进到tryAcquire(arg)里又调用了nonfairTrytAcquire(acquires)

//JDK源码
public class ReentrantLock implements Lock, java.io.Serializable {
    
	public void lock(){
        sync.acquire(1);
    }
    
    static final NonfairSync extends Sync{
        protected final boolean tryAcquire(int acquire){
            return nonfairTrytAcquire(acquires);
	   }
    }
}


5.nonfairTrytAcquire(acquires)方法

//JDK源码
public class ReentrantLock implements Lock, java.io.Serializable {
    
	public void lock(){
        sync.acquire(1);
    }
    
    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;
    }
}


nonfairTrytAcquire(acquires)我们读进去会发现它的里面就调用到了state这个值,到这里我们就接上了上一章讲的,nonfairTrytAcquire(acquires)里是这样的,

  1. 首先拿到当前线程,拿到state的值,然后进行if判断,
  2. 如果state的值为0,说明没人上锁,没人上锁怎么办呢?就给自己上锁,当前线程就拿到这把锁,拿到这个把锁的操作用到了CAS(compareAndSetState)的操作,从0让他变成1,state的值设置为1以后,设置当前线程是独一无二的拥有这把锁的线程,
  3. 否则如果当前线程已经占有这把锁了,怎么办?很简单我们在原来的基础上加1就可以了,这样就能拿到这把锁了,就重入,前者是加锁后者是重入

6.如果拿不到呢?如果拿不到它实际上是调用了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();
	}
}

  • 如果拿不到呢?如果拿不到它实际上是调用了acquireQueued()方法,acquireQueued()方法里又调用了addWaiter(Node.EXCLUSIVE)然后后面写一个arg(数值1),方法结构是这样的acquireQueued(addWaiter(Node.EXCLUSIVE),arg)通过acquireQueued这个方法名字你猜一下这是干什么的,你想如果是我得到这把锁了,想一下后面的acquireQueued是不用运行的,如果没有得到这把锁,后面的acquireQueued()才需要运行,那么想一下没有得到这把锁的时候它会运行什么呢?他会运行acquireQueued,Queued队列,acquire获得,跑到队列里去获得,那意思是什么?排队去,那排队的时候需要传递两个参数,第一个参数是某个方法的返回值addWaiter(Node.EXCLUSIVE),来看这个方法的名字addWaiter,Waiter等待者,addWaiter添加一个等待者,用什么样的方式呢?Node.EXCLUSIVE排他形式,意思就是把当线程作为排他形式扔到队列里边。

如果拿不到锁,就把当钱线程作为排他形式扔到队列里边

addWaiter()方法----加入双向等待队列

  • 我们来说一下这个addWaiter()方法,这个方法意思是说你添加等待者的时候,使用的是什么类型,如果这个线程是Node.EXCLUSIVE那么就是排他锁Node.SHARED就是共享锁,首先是获得当前要加进等待者队列的线程的节点,然后是一个死循环,这意思就是说我不干成这件事我誓不罢休,那它干了一件什么事呢?

//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(mode);
        for(;;){
            //回想一下AQS数据结构图
		   Node oldTail = tail;
            if(oldTail != null){
                //把我们这个新节点的前置节点设置在等待队列的末端
				node.setPrevRelaved(oldTail);
                //CAS操作,把我们这个新节点设置为tail末端
                 if(compareAndAetTail(oldTail,node)){
					oldTail.next = node;
                      return node;
                 }
            }else{
                initializeSuncQueue();
            }
        }
    }
}

你想想想看,我们回想一下AQS数据结构图,就是他有一个int类型的数叫state,然后在state下面排了一个队列,这个队列是个双向的链表有一个head和一个tail

  1. 现在你要往这个队列中加一个节点上来,要排队嘛,我们仔细想一下加节点的话,应该得加到这个队列的末端是不是?它是怎么做到的呢?首先把tail记录在oldTail里,oldTail指向这个tail了,如果oldTail不等于空,它会把我们这个新节点的前置节点设置在这个队列的末端,
  2. 接下来再次用到CAS操作,把我们这个新的节点设置为tail,整段代码看似繁琐,其实很简单,就是要把当前要加进等待者队列的线程的节点加到等待队列的末端,这里提一点,加到末端为什么要用CAS操作呢?因为CAS效率高,这个问题关系到AQS的核心操作,理解了这一点,你就理解了AQS为什么效率高,我们接着讲源码,这个增加线程节点操作,如果没有成功,那么就会不断的试,一直试到我们的这个node节点被加到线程队列末端为止,意思就是说,其它的节点也加到线程队列末端了,我无非就是等着你其它的线程都加到末端了,我加最后一个,不管怎么样我都要加到线程末端去为止。

acquireQueued()----队列里尝试去获得锁


解读acquireQueued()这个方法,这个方法的意思是,在队列里尝试去获得锁,在队列里排队获得锁,那么它是怎么做到的呢?我们先大致走一遍这个方法,

  1. 首先在for循环里获得了Node节点的前置节点,
  2. 然后判断如果前置节点是头节点,并且调用tryAcquire(arg)方法尝试一下去得到这把锁,获得了头节点以后,你设置的节点就是第二个,你这个节点要去和前置节点争这把锁,这个时候前置节点释放了,如果你设置的节点拿到了这把锁,拿到以后你设置的节点也就是当前节点就被设置为前置节点,如果没有拿到这把锁,当前节点就会阻塞等着,等着什么?等着前置节点叫醒你,所以它上来之后是竞争,怎么竞争呢?如果你是最后节点,你就下别说了,你就老老实实等着,如果你的前面已经是头节点了,说明什么?说明快轮到我了,那我就跑一下,试试看能不能拿到这把锁,说不定前置节点这会儿已经释放这把锁了,如果拿不着阻塞,阻塞以后干什么?等着前置节点释放这把锁以后,叫醒队列里的线程,我想执行过程已经很明了了,

打个比方,有一个人,他后面又有几个人在后面排队,这时候第一个人是获得了这把锁,永远都是第一个人获得锁,那么后边来的人干什么呢?站在队伍后面排队,然后他会探头看他前面这个人是不是往前走了一步,如果走了,他也走一步,当后来的这个人排到了队伍的第二个位置的时候,发现前面就是第一个人了,等这第一个人走了就轮到他了,他会看第一个人是不是完事了,完事了他就变成头节点了,就是这么个意思。


锁释放后,只会通知队列中的第一个线程去竞争锁,减少了并发冲突。(ZK的分布式锁,为了避免惊群效应,也使用了类似的方式:获取不到锁的线程只监听前一个节点)

小结;

  • 源码读这里我们可以总结得出,AQS(AbstractQueuedSynchronizer)的核心就是用CAS(compareAndSet)去操作head和tail,就是说用CAS操作代替了锁整条双向链表的操作

几个重要知识点

1.AQS为什么效率这么高?

通过AQS是如何设置链表尾巴的来理解AQS为什么效率这么高

我们的思路是什么呢?假如你要往一个链表上添加尾巴,尤其是好多线程都要往链表上添加尾巴,我们仔细想想看用普通的方法怎么做?

  1. 第一点要加锁这一点是肯定的,因为多线程,你要保证线程安全,一般的情况下,我们会锁定整个链表(Sync),我们的新线程来了以后,要加到尾巴上,这样很正常,但是我们锁定整个链表的话,锁的太多太大了,现在呢它用的并不是锁定整个链表的方法,而是只观测tail这一个节点就可以了,怎么做到的呢?
  2. compareAndAetTail(oldTail,node),中oldTail是它的预期值,假如说我们想把当前线程设置为整个链表尾巴的过程中,另外一个线程来了,它插入了一个节点,那么仔细想一下Node oldTail =tail;的整个oldTail还等于整个新的Tail吗?不等于了吧,那么既然不等于了,说明中间有线程被其它线程打断了,那如果说却是还是等于原来的oldTail,这个时候就说明没有线程被打断,那我们就接着设置尾巴,只要设置成功了OK,compareAndAetTail(oldTail,node)方法中的参数node就做为新的Tail了,

所以用了CAS操作就不需要把原来的整个链表上锁,这也是AQS在效率上比较高的核心。

2.为什么是双向链表?

其实你要添加一个线程节点的时候,需要看一下前面这个节点的状态,如果前面的节点是持有线程的过程中,这个时候你就得在后面等着,如果说前面这个节点已经取消掉了,那你就应该越过这个节点,不去考虑它的状态,所以你需要看前面节点状态的时候,就必须是双向的。
,g_se,x_16)

3. VarHandle

jdk9以后才有的

  • 我们再来讲一个细节,我们看addWaiter()这个方法里边有一个node.setPrevRelaved(oldTail),这个方法的意思是把当前节点的前置节点写成tail,进入这个方法你会看到PREV.set(this,p),那这个PREV是什么东西呢?


当你真正去读这个代码,读的特别细的时候你会发现,PREV有这么一个东西叫VarHandle,这个VarHandle是什么呢?这个东西实在JDK1.9之后才有的,我们说一下这个VarHandle,Var叫变量(variable),Handle叫句柄,打个比方,比如我们写了一句话叫Object o= new Object(),我们new了一个Object,这个时候内存里有一个小的引用“O”,指向一段大的内存这个内存里是new的那个Object对象,那么这个VarHandle指什么呢?指的是这个“引用” 我们思考一下,如果VarHandle代表“引用”,那么VarHandle所代表的这个值PREV是不是也这个“引用”呢?当然是了。这个时候我们会生出一个疑问,本来已经有一个“O”指向这个Object对象了,为什么还要用另外一个引用也指向这个对象,这是为什么?

通过这个handle可以做什么事呢?

通过这个handle可以做什么事呢?handle.compareAndSet(t,9,10),做原子性的修改值,我通过handle.compareAndSet(t,9,10)把9改成10改成100,这是原子性的操作,你通过x=100 ,它会是原子性的吗?当然int类型是原子性的,但是long类型呢?就是说long类型连x=100都不是原子性的,所以通过这个handle可以做一些compareAndSet操作(原子操作),还可以handle.getAndAdd()操作这也是原子操作

比如说你原来写x=x+10,这肯定不是原子操作,因为当你写这句话的时候,你是需要加锁的,要做到线程安全的话是需要加锁的,但是如果通过handle是不需要的,所以这就是为什么会有VarHandle,VarHandle除了可以完成普通属性的原子操作,还可以完成原子性的线程安全的操作,这也是VarHandle的含义。

VarHandle除了可以完成普通属性的原子操作,还可以完成原子性的线程安全的操作,这也是VarHandle的含义。

  • 在JDK1.9之前要操作类里边的成员变量的属性,只能通过反射完成,
  • 用反射和用VarHandle的区别在于,VarHandle的效率要高的多,反射每次用之前要检查,VarHandle不需要,VarHandle可以理解为直接操纵二进制码,所以VarHandle反射高的多

以上是关于提升--09---AQS源码解析的主要内容,如果未能解决你的问题,请参考以下文章

Vue3 源码解析:静态提升

VsCode 代码片段-提升研发效率

提升--17---线程池--03----ThreadPoolExecutor源码解析

死磕性能调优技巧,性能指标提升一倍以上!

如何阅读源码?这篇《Android开发源码精编解析》助你进阶提升

源码阅读最新Android开发源码精编解析,技术提升必备