连肝4天,这瞬间戳中面试官小心心的AQS大餐,给大家安排上!

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

如上图,左边是抽象队列同步器,而右边则是使用队列同步器实现的功能——锁、信号量、发令枪等。
可以先不看源码,咱们自己思考,要以纯代码的方式实现应当考虑哪些问题?

  1. 线程互斥:可以使用state状态进行判断,state=0,则可以获取到锁,state>0,则不能获取。
  2. 排队等候:不能获取锁的线程应当存储起来,当锁释放后可以继续获取锁执行。
  3. 线程唤醒:当锁释放后,处于等待状态的线程应当被唤醒。
  4. 锁重入 : 如何解决同一个进入多个加锁的方法(不解决的话分分钟死锁给你看)。

对于1、2两点,难度应带不大,而3、4两点如何去设计呢?我们通过伪代码预演操作流程。

在业务端,是这样操作的。

加锁
{需要被锁住的代码}
释放锁

加锁与释放锁的逻辑

if(state == 0)
获取到锁
set(state == 1)
else
继续等待
while(true){
if(state == 0)
再次尝试获取锁
}

这样设计之后,整个操作流程再次变成了串行操作。

这和我们去食堂排队打饭是一样的,食堂不可能为每个学生都开放一个窗口,所以多个学生就会争抢有限的窗口,如果没有一定的控制,那么食堂每到吃饭的时候都是乱套的,一群学生围着窗口同时去打饭,想想都是多么的恐怖。而由此出现了排队的机制,一个窗口同一时间打饭的人只能有一个,当前一个人离开窗口后,后面排队的学生才能去打饭。

源码解读

下面我们深入JDK源码,领略大师级的代码设计。
业务调用代码:

package demo.aqs;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockMoney {
Lock lock = new ReentrantLock();
/**

  • 假设现在账户有1000块钱
    /
    private int money = 1000;
    //private int money = 1000;
    /
    *
  • 取钱
    */
    public void drawMoney(){
    lock.lock();
    this.money–;
    lock.unlock();
    }
    public static void main(String[] args) throws InterruptedException {
    LockMoney money = new LockMoney();
    for(int i=0; i<1000; i++){
    new Thread(() -> {
    try {
    Thread.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    money.drawMoney();
    },i + “”).start();
    }
    Thread.sleep(2000);
    System.out.println(“当前账户余额:” + money.money);
    }
    }

追踪Lock方法:
直接看源码基本一会儿就晕车,我尝试绘制出lock方法的调用链路。然后结合源码解释。

大家跟着箭头走一遍源码,多多少少能够体会到AQS的实现机制。

NonfairSync.lock

final void lock() {
//CAS尝试将state从0更新为1,更新成功则执行if下面的代码。
if (compareAndSetState(0, 1))
//获取锁成功,执行线程执行
setExclusiveOwnerThread(Thread.currentThread());
else
//获取锁失败,线程入队列
acquire(1);
}

看到这段代码,是不是瞬间明白前面提到的1、2两点问题。首先compareAndSetState方法是使用Unsafe直接操作内存并且使用乐观锁的方式,能够保证有且仅有一个线程能够操作成功,是多线程安全的。即设置将state设置为1成功的线程能够抢占到锁(线程互斥),而没有设置成功的线程将进行入队操作(排队等候),这样感觉瞬间明朗了许多,那我们接着往下看。

AbstractQueuedSynchronizor.acquire

public final void acquire(int arg) {
//tryAcquire失败并且acquireQueued成功,则调用selfInterrupt
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//当线程获取锁失败并且线程阻塞失败会中断线程
selfInterrupt();
}

AbstractQueuedSynchronizor的tryAcquire方法,其最终调用到了Sync的nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前锁的状态值
int c = getState();
// state = 0,表示当前锁为空闲状态,其实这一段代码和前面lock的方法是一样的
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//不等于0 则判断当前线程是否为持有锁的线程,如果是则执行代码,这里解决了重入锁问题
else if (current == getExclusiveOwnerThread()) {
//当前状态值 + 1(可以看前面的传参)
int nextc = c + acquires;
// 囧, 这里是超出了int的最大值才会出现这样的情况
if (nextc < 0) // overflow
throw new Error(“Maximum lock count exceeded”);
//更新state的值
setState(nextc);
return true;
}
return false;
}

通过阅读源码,可以发现,tryAcquire方法在当前线程获取锁成功或者是重入锁的情况下返回true,否则返回false。而同时这个方法解决了上面提到的第4点锁重入的问题。ok,感觉越来越接近真相了,接着看addWaiter方法。
理解addWaiter方法的代码,先看方法中用的得Node对象。 Node对象是对Thread对象的封装,使其具有线程的功能,同时他还有prev、next等属性。那么很明了,Node是一个链表结构的对象

//前一个结点
volatile Node prev;
//下一个结点
volatile Node next;

同时AbstractQueuedSynchronizor中包含head、tail属性

//Node链表的头结点
private transient volatile Node head;
//Node链表的尾结点
private transient volatile Node tail;

private Node addWaiter(Node mode) {
//将当前线程包装为Node对象
Node node = new Node(Thread.currentThread(), mode);
//获取尾节点,当这段代码第一次运行的时候,并没有尾结点
//所以肯定值为null,那么会执行下面的enq方法
Node pred = tail;
//当再次运行代码的时候,尾结点不再为null(enq方法初始化了尾结点,可以先往下看enq方法源码)
if (pred != null) {
//当前结点的前置结点指向之前的尾结点
node.prev = pred;
//CAS尝试将尾结点从pred设置为node
if (compareAndSetTail(pred, node)) {
//设置成功则将pred的next结点执行node
pred.next = node;
return node;
}
}
enq(node);
return node;
}

上面的解释听着有点绕脑袋。

不着急,我们先看enq方法

private Node enq(final Node node) {
//死循环
for (;😉 {
//获取尾结点
Node t = tail;
//尾结点为空,则初始化尾结点和头结点为同一个新创建的Node对象
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//将当前结点设为为尾结点,并将前一个尾结点的next指向当前结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
//退出循环
return t;
}
}
}
}

enq具体做了什么事情呢:

  1. 第一次循环,初始化头结点与尾结点 new Node()
  2. 第二次循环,将当前线程封装的Node对象设置为尾结点,并将前一个尾结点的next指向此Node

这里需要一些时间 + 空间的想象力,但如果对链表结构比较熟悉的话,这里理解也是不太困难的。
我们动态的想一想执行过程:

  1. 第一个线程进入lock方法,此时是肯定可以获取到锁,直接执行,不会进入到addWaiter方法
  2. 第二个线程进入lock方法,我们假设第一个线程还没有释放锁,此时进入执行enq方法,enq进行链表的初始化。

  1. 第三个线程以及更多的线程进入lock方法,此时不再执行enq方法,而是在初始化之后的链表进行链接。

acquireQueued

final boolean acquireQueued(final Node node, int arg) {
//局部变量
boolean failed = true;
try {
//局部变量
boolean interrupted = false;
//死循环
for (;😉 {
//获取前置结点
final Node p = node.predecessor();
//前置结点为head并且尝试获取锁成功,则不阻塞
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//阻塞操作 , 判断是否应该阻塞 并且 阻塞是否成功
if (
//是否在抢占锁失败后阻塞
shouldParkAfterFailedAcquire(p, node) &&
//Unsafe操作使线程阻塞
parkAndCheckInterrupt())
interrupted = true;

最后

由于文案过于长,在此就不一一介绍了,这份Java后端架构进阶笔记内容包括:Java集合,JVM、Java并发、微服务、SpringNetty与 RPC 、网络、日志 、Zookeeper 、Kafka 、RabbitMQ 、Hbase 、MongoDB、Cassandra 、Java基础、负载均衡、数据库、一致性算法、Java算法、数据结构、分布式缓存等等知识详解。

image

本知识体系适合于所有Java程序员学习,关于以上目录中的知识点都有详细的讲解及介绍,掌握该知识点的所有内容对你会有一个质的提升,其中也总结了很多面试过程中遇到的题目以及有对应的视频解析总结。

image

image

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
5)]

[外链图片转存中…(img-fhO6zK04-1713366081575)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-4yEpTfBo-1713366081576)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值