- 读源码的时候自己去尝试画图!!!—->泳道图(UML图的一种)—->哪个类里调用哪个方法
- 子类可能会用到父类的方法(所以要把父子关系看清楚了)
- 读源码时先读骨架!!!
- AQS是所有的锁的核心
- 模板方法模式:流程是一样的就像一个模板一样,但其中的方法(每一个步骤的具体处理)是由具体的子类去实现的,当执行到流程中的某一步的时候就会调用具体的子类实现,所以这就叫做钩子函数(钩子勾着子类的实现)
- 回调方法与钩子函数的区别
- 所有的设计模式(90%以上)全是多态
相关图解
执行步骤
- 公平与非公平的概念:上来就先排队的为公平;不管队列中有没有人在等着,上来就抢为非公平
- nonfairTryAcquire方法:非公平地去获取锁
- 同步队列和等待队列是两个不同的概念
- 同步队列指队列中实现了同步,多线程可以并发安全地访问
- 等待队列指一把锁所对应的队列,至于这个队列是同步的还是不同步的看具体实现(是不同的概念),这是属于锁的内部实现,是jvm实现的,是怎样的还不好说
- 是不同的东西,是从不同的角度来看待的
- AQS的结构:
- AQS队列也被称为CLH队列(CLH是三个人,AQS是CLH的变种)
- AQS的核心结构是什么,核心就是state(volatile的int类型的数)
- state的含义是自己决定的,随你来定,随子类来定!
- state之下跟着一个队列,这个队列是AQS内部维护的一个队列
- 队列中每一个都是node,是一个节点
- node中最重要的是保存的Thread thread;线程变量
- 线程队列—->有pre,有next
- 队列中装的是线程,一个node可以指向前面的也可以指向后面的,所以叫双向链表
- AQS的核心是一个state和监控这个state的双向链表,链表中的节点装的是线程
- 哪个线程得到了这把锁,哪个线程要等待要进入这个队列中
- 双向链表有一个head和tail
- 当某个node得到这把锁,去改state成功之后,就说明这个线程持有了这把锁
- 一个线程去看state为0,就去拿这把锁,拿到之后state变为1。当这个线程执行结束之后,其他线程会来抢这把锁,假如是非公平,那么抢不着就进入队列中继续等待
- 百度面试题:
- 为什么AQS的底层是CAS+volatile?
- 调用lock,在lock中调用了AQS中的acquire方法,acquire方法是模板方法的模板,在模板中调用了tryAcquire方法,这个方法由子类重写(这里是NonfairSync类),tryAcquire方法又调用了nonfairTryAcquire方法(这时必须了解AQS,不了解AQS是读不懂nonfairTryAcquire方法的)
- 调用AQS类中的getState方法,state是一个volatile类型的数(成员变量)
- state的含义是自己决定的,随你来定,随子类来定!
- 在ReentrantLock中state是表示加锁(1)和解锁(0)的(可重入),用来记录这个线程重入了多少次(1->2->3;3->2->1)
- 在CountDownLatch中state表示要countDown多少次才能够解开
- 如果state等于0,就用CAS的方法去改state(期望值为0,期望值改为1)
- 如果该成功了,就把当前线程设为独占这把锁的线程,说明已经得到了这把锁,且这把锁是互斥的,别人得不到(因为别人来的时候就变成了1)
- 如果state不等于0,并且当前独占的线程就是我当前的线程时,将state往后加(可以加多个),表示可重入
- tryAcquire抢到了就抢到了,就去运行,没抢到就进队列等待
- 如果没有拿到这把锁时,调用acquireQueued,其中acquireQueued的参数是addWaiter的返回值和arg(即个数)—->acquireQueued跑到队列中去获得—->去队列中排队
- addWaiter排他地添加一个等待者(扔到队列里)
- 先new一个node,然后进入一个死循环,不干成这件事誓不罢休—->这是获取不到锁,必须加到等待队列的情形,要加在tail的后面
- 如果tail不为空,那么就将new出来的node的pre指针设置为tail,再通过CAS将当前的tail改为新new出来的node
- 如果tail为空,那么就初始化同步队列
- node.setPrevRelaxed(oldTail)不会有影响,因为new了一个node之后,每一次循环CAS都会设置一遍刚刚new出来的节点的前置节点
- addWaiter方法返回的是刚刚加入队列中的那个节点(这时很有可能有别的线程又加到该节点后面了),然后执行acquireQueued,在队列中尝试去获得锁(在队列里排队去获得锁)
- acquireQueued方法:获取新入队节点的前一个节点(前置节点)
- 如果前置节点是head头结点,并且再次用tryAcquire方法去尝试一下去获取锁
- 如果获取到了,就设置当前的头节点为此时的node—->这表示当前置节点是head节点的时候去跟前置节点竞争,前置节点刚好释放了,然后此时加入的节点就拿到了
- 否则tryAcquire时没有拿到锁的时候,应该park阻塞、停住,等着前置节点叫醒你
- 如果不是排在第二个,就自己去重试;假如能够轮到自己,就把前置节点干掉,自己就变成头结点了
- 是如何竞争的?
- 如果是后面的(比如最后的)节点,就没有竞争的资格,只能老老实实地等待
- 如果前面已经是头结点了,说明快到我了,我就跑一下,看能不能拿到这把锁,说不定前置节点这儿已经释放这把锁了;如果拿不到,依然阻塞
- 阻塞了之后就等着前面的节点(前置节点)释放这把锁之后,叫醒队列中的东西
- 永远是头结点获得这把锁
- 如果前置节点是head头结点,并且再次用tryAcquire方法去尝试一下去获取锁
- 其中一个细节(知识点):addWaiter中的node.setPrevRelaxed(oldTail)把当前节点的前置节点写成之前的队列tail,这里设置的时候用的是PREV.set(this, p);
- PREV是VarHandle,VarHandle在jdk1.9之后才有的,jdk9之前想要操作类对象里的成员变量只能运用反射!!!
- VarHandle是变量句柄,指的是引用变量指向对象的那个引用(代表的是那个引用),VarHandle所代表的值也能指向那个对象
- VarHandle是指向某个变量的引用
- 为什么要另外一个变量指向同一个对象变量(引用变量和这个VarHandle变量都指向了同一个对象)?多此一举?
- 读源码时,固定的写法可以不用关注
- 如下图例子,int(x)没引用,但是加handler之后就可以想象成有一个引用指向了那块内存
- 有了handler之后,就可以直接操纵所指向的那个变量的值了(可以读,可以写)
- 更重要的是,通过handler可以原子性地修改这个值(compareAndSet)—->int类型做x=100;时是原子性的,但long类型做x=100;时不是原子性的
- handler可以进行一些CAS操作:compareAndSet、getAndAdd……等原子性操作,之前直接做这些操作时是需要加锁的,而通过handler是不需要进行加锁的(CAS)
- 有了VarHandler,普通的属性也能变成原子操作!!!(之前要用Atomic类进行原子操作!)
- 除了完成普通属性的操作外,还可以完成线程安全性的操作
- 下面的例子中用一个静态的VarHandler指向了T01_HelloVarHandler类型的对象中的一个int类型的x变量,在使用的时候只要传入目前操作的对象即可,不必再与具体的对象进行绑定—->与反射的使用方式有点类似
- VarHandler是native的,用C/C++写的源码,估计是直接对内存进行操作—->实际上是调用了cpu的原语
- 用反射和VarHandler的区别:
- VarHandler效率高得多
- 反射每次用都要做检查,VarHandler不需要
- VarHandler可以理解为直接操纵二进制码
- 实际中很少用这个,一般很少用这个,只有框架或者jdk源码才可能会用这个—->面试的时候造火箭用的
- 其实就是为了提高整个juc的效率,用了好多办法,jdk增加的代码
- AQS怎么释放锁的,释放锁之后怎么通知后置节点的,比较简单,自己去读(unlock)
- 进入队列的时候用CAS的方法往尾巴上加
- 只有一个队列但有多个线程抢锁要往尾巴上加(要进入队列去排队)
- 都往上加,谁先抢到,谁后抢到
- 不想出现一个尾巴分岔的问题,只能一个来了先加上,另一个来了就加在之前的后面
- 最简单的方法是加锁,但AQS不用加锁,是用CAS的方法
- 加锁会锁定整个链表,锁的太多太大了
- 故不用锁整个链表的方法,而是对tail进行CAS,只观测这个tail!!!(只看尾巴)
- 对尾巴进行CAS的时候,期望的尾巴是oldtail,假如这过程中有别的线程加入了队列,尾巴就会改变,不是oldtail了,此时会失败(设置tail时只是将tail这个引用指向了新的节点,并没有改变之前链表中节点的连接情况)
- 用CAS后不需要对整个链表上锁了,效率会提高
- CAS有自己的实现,可以决定是不断的尝试,还是尝试一次后就停止返回出成功还是失败
- 不断的循环可以放在CAS中,可以放在CAS外面包住CAS,来不断地循环!!!(是两种实现方式)——>自旋
- 没有成功就不断不断地试,一直试到这个节点被加到尾巴上为止;不管怎么样节点都要加到尾巴上,只不过是个顺序先后的问题
- AQS中的CAS最重要的是往尾巴上加东西的时候的CAS,而不是setState的时候的CAS
- AQS中等待队列为什么要用双向的链表呢?
- 因为加进来的节点要先看一下前面节点的状态
- 如果前面节点正在持有线程,就赶紧在那等着
- 如果前面节点已经被取消掉了,就越过那个节点,就不去考虑他的状态了
- 需要看前面节点的状态时就要用双向链表
AQS的核心是一个共享的数据和一堆互相竞争(抢夺数据)的线程
AQS的核心就是用CAS去操作tail,在前面加锁时去尝试获得锁的时候也用的是CAS操作head
用CAS操作替代了锁整条链表的操作
🤏随想
- 同步队列、阻塞队列、等待队列、异步队列、条件队列
- 同步队列, 首先是获取锁失败,调用park阻塞线程,封装为Node ,然后CAS添加到队列的末尾,然后 在同步队列里面,有头结点的下一个节点被remove掉之后,显式的 调用了 unpark 方法唤醒后继节点去获取锁。这里锁的获取是没有 自旋 的,Node 加入尾部是在CAS循环的。
- 等待队列,是已经获取到锁的线程,需要用到其他线程的数据,主动调用 wait() 方法,并且释放锁,唤醒同步队列的后继节点(非公平不用唤醒),然后当前节点构造新的Node进入等待队列,当有其他 在同步队列里面 已经获得锁的线程调用 notify()(或者 signal())之后,才会重新加入同步队列,尝试获取锁。 等待队列的节点也调用了 park () 方法阻塞。
- 阻塞队列(BlockingQueue)和同步队列(SynchronousQueue)
- state的含义是自己决定的,随你来定,随子类来定!