AQS面试题总结

一:线程等待唤醒的实现方法

方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

必须都在synchronized同步代码块内使用,调用wait,notify是锁定的对象;
notify必须在wait后执行才能唤醒;public class LockSupportDemo1 {public static void main(String[] args) {Object objectLock = new Object();/*** t1	 -----------come in* t2	 -----------发出通知* t1	 -------被唤醒*/new Thread(() -> {synchronized (objectLock) {System.out.println(Thread.currentThread().getName() + "\t -----------come in");try {objectLock.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "\t -------被唤醒");}}, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {synchronized (objectLock) {objectLock.notify();System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");}}, "t2").start();}
}

方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程

必须在lock同步代码块内使用;
signal必须在await后执行才能唤醒;public class LockSupportDemo2 {public static void main(String[] args) {Lock lock = new ReentrantLock();Condition condition = lock.newCondition();new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + "\t -----------come in");condition.await();System.out.println(Thread.currentThread().getName() + "\t -----------被唤醒");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}, "t1").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {lock.lock();try {condition.signal();System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");} finally {lock.unlock();}}, "t2").start();}
}

方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

不需要锁块;
unpark()可以在park()前唤醒;public class LockSupportDemo {public static void main(String[] args) {/*** t1	 -----------come in* t2	 ----------发出通知* t1	 ----------被唤醒*/Thread t1 = new Thread(() -> {System.out.println(Thread.currentThread().getName() + "\t -----------come in");try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}LockSupport.park();System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");}, "t1");t1.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {LockSupport.unpark(t1);//指定需要唤醒的线程,可以先给t1发放许可证,t1再被锁定,此时t1可以立马被唤醒System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");}, "t2").start();}
}

二: 介绍一下LockSupport

LockSupport是什么: LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只有两个值,1和0;默认是0,许可证不能超过1;
park()方法:调用park方法,当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。
unpack(thread)方法: 调用unpack方法,就会将thread线程的许可证peimit发放,唤醒处于阻塞状态的指定线程。

面试题:
1:LockSupport为什么可以先唤醒线程后阻塞线程但不会阻塞?
答:因为unpark()方法获得了一个许可证,许可证值为1,再调用park()方法,就可消费这个许可证,所以不会阻塞;

2:为什么唤醒两次后阻塞两次,最终还是会阻塞?
答:如果线程A调用两遍park(),线程B调用两边unpark(),那么只会解锁一个park(),因为许可证最多只能为1,不能累加;

三:AQS是什么

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列完成资源获取排队工作,将每条要去抢占资源的线程封装成一个NODE节点来实现锁的分配,通过CAS完成对State值的修改,
AQS的本质是一个双向队列加一个状态为state

五:公平锁与非公平锁的区别

公平锁: 多个线程按照线程调用lock()的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

六: 非公平锁加锁的源码分析

tryAcquire(arg):尝试获取锁
addWaiter(Node.EXCLUSIVE):添加到同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):队列里自旋等待获取等

第一步、tryAquire

先获取当前AQS 的state的值,判断是否为0,如果为0表示没有人抢占,此刻他抢占,返回true,抢占锁后就完事了;

   // 这里调用进入非公平锁的tryAcquireprotected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}// 具体代码在这里final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 若当前无其他线程抢占锁,则抢占;if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}//如果已获取锁的线程再调用lock()则state值+1,这里就是可重入的原理else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}// 都不是则返回falsereturn false;}

第二步、addWaiter

创建队列的节点,首先就先new出来一个节点,由于刚开始当前队列没有节点,因此进入enq()方法;

enq方法插入节点是死循环:
第一次循环,由于tail为空,他先创建一个空的node节点,作为头节点,此时waitStatus=0,然后将head指向该头节点,并将tail指针也指向head;

第二次循环,他将待插入node节点(线程B)的前置指针指向tail指向的节点(头节点),然后CAS将tail指向当前待插入节点(线程B),再让原来的tail指向的节点(头节点)的next域指向当前节点,这样就完成了节点(线程B)插入队尾,完成链式结构,跳出循环;
在这里插入图片描述

private Node addWaiter(Node mode) {// 创建一个节点 modeNode node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;// 刚开始tail是null,如果tail有值了就将node插入队尾;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 若队列为空,则插入节点enq(node);return node;}private Node enq(final Node node) {for (;;) { // 死循环Node t = tail;if (t == null) { // 初始下tail为null,因此创建一个头节点if (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;// 第二次循环,队列不为空,就将该节点插入队尾if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

第三步:aquireQueued

这个方法依旧是死循环。
第一次循环:首先predecessor()取出的就是前置节点,p就是链表中的头节点,然后进入判断,当前确实是头节点,然后再次尝试tryAcquire(),由于线程A并没有释放锁,因此,只能进入shouldParkAfterFailedAcquire()方法;

第二次循环,再次进入shouldParkAfterFailedAcquire(),这一次由于ws=-1,因此返回true,并进入parkAndCheckInterrupt()方法;这里会调用LockSupport.park()将线程挂起,此刻线程B就阻塞再这里了。
在这里插入图片描述

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点if (p == head && tryAcquire(arg)) {//由于线程A占用,尝试获取失败setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程B会进入这里interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;// 头节点的waitStatus=0if (ws == Node.SIGNAL)// -1return true;if (ws > 0) {do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 将头节点的waitStatus设置成-1}return false;
}private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}

七:非公平锁解锁的具体流程

线程A就先去获取AQS的state,并对应减去1个,并设置当前占有线程为null,然后找到头节点去调用unparkSuccessor(head),他将头节点的状态从-1设置为0,然后唤醒线程B;

在这里插入图片描述
在这里插入图片描述

// 执行ReentrantLock.unlock()
public void unlock() {sync.release(1);
}// AQS.release()
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}// 执行ReentrantLock.tryRelease()
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;}private void unparkSuccessor(Node node) {int ws = node.waitStatus;// 头节点是-1if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 头节点设置为0Node s = node.next;// 线程Bif (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) LockSupport.unpark(s.thread);//唤醒线程B
}

线程B还锁在parkAndCheckInterrupt()方法中,解锁后开始第三次循环,第三次循环发现前置节点是头,且可以占用锁,因此线程B获取到锁并进入第一个if;然后重新设置头节点,将头指向线程B,将原头节点剔除队列,然后将线程B设置成头节点。

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点if (p == head && tryAcquire(arg)) {//目前锁无占用,进入此处setHead(node); // 重新设置头节点p.next = null; // help GCfailed = false;return interrupted; // 被改为true}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// 线程B从这里唤醒interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}// 修改头节点          
private void setHead(Node node) {head = node;node.thread = null;node.prev = null;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/127923.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Python语言速回顾】——数据可视化基础

目录 引入 一、Matplotlib模块&#xff08;常用&#xff09; 1、绘图流程&常用图 ​编辑 2、绘制子图&添加标注 ​编辑 3、面向对象画图 4、Pylab模块应用 二、Seaborn模块&#xff08;常用&#xff09; 1、常用图 2、代码示例 ​编辑 ​编辑 ​编辑 ​…

一个基于Excel模板快速生成Excel文档的小工具

介绍 DocumentGenerator是一个Excel快速生成工具&#xff0c;目标以后还能实现Word、pdf等的文件的生成。该程序独立运行&#xff0c;可通过HTTP接口调用其生成接口。 典型使用场景为如下&#xff1a; 使用者编写模板文件使用者准备模板文件的填充JSON数据内容使用者通过网络…

网络套接字编程(二)

网络套接字编程(二) 文章目录 网络套接字编程(二)简易TCP网络程序服务端创建套接字服务端绑定IP地址和端口号服务端监听服务端运行服务端网络服务服务端启动客户端创建套接字客户端的绑定和监听问题客户端建立连接并通信客户端启动程序测试单执行流服务器的弊端 多进程版TCP网络…

CCF_A 计算机视觉顶会CVPR2024投稿指南以及论文模板

目录 CVPR2024官网&#xff1a; CVPR2024投稿链接&#xff1a; CVPR2024 重要时间节点&#xff1a; CVPR2024投稿模板: WORD: LATEX : CVPR2024_AuthorGuidelines CVPR2024投稿Topics&#xff1a; CVPR2024官网&#xff1a; https://cvpr.thecvf.com/Conferences/2024CV…

【Linux】常见指令以及具体其使用场景

君兮_的个人主页 即使走的再远&#xff0c;也勿忘启程时的初心 C/C 游戏开发 Hello,米娜桑们&#xff0c;这里是君兮_&#xff0c;随着博主的学习&#xff0c;博主掌握的技能也越来越多&#xff0c;今天又根据最近的学习开设一个新的专栏——Linux&#xff0c;相信Linux操作系…

【嵌入式开发学习02】esp32cam烧录human_face_detect实现人脸识别

Ubuntu20.04系统为esp32cam烧录human_face_detect 1. 下载esp-dl2. 安装esp-idf3. 烧录human_face_detect 如果使用ubuntu 16.04在后续的步骤中会报错如下&#xff0c;因为ubuntu 16.04不支持glibc2.23以上的版本&#xff08;可使用strings /lib/x86_64-linux-gnu/libc.so.6 | …

服务号改订阅号怎么弄

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;很多小伙伴想把服务号改为订阅号&#xff0c;但是不知道改了之后具体有什么作用&#xff0c;今天跟大家具体讲解一下。首先我们知道服务号一个月只能发四次文章&#xff0c;但是订阅号每天都可以发…

JVM——类的生命周期(加载阶段,连接阶段,初始化阶段)

目录 1.加载阶段2.连接阶段1.验证2.准备3.解析 3.初始化阶段4.总结 类的生命周期 1.加载阶段 ⚫ 1、加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息。 程序员可以使用Java代码拓展的不同的渠道。 ⚫ 2、类加载器在加载完类…

C++和 C 混合编程处理

原因是因为有很多功能是用 C 语言开发的&#xff0c;而 C是兼容 C 的&#xff0c;C应该能直接使用这些功能&#xff0c;那么我们把 C调用 C 实现的功能的这个做法&#xff0c;称为混合编程 但是用 C 开发的功能&#xff0c;很可能已经用 C 编译器编程成目标文件(或打包成库了)…

3.16每日一题(区间在现求定积分)

解法一&#xff1a; 1、二倍角化简&#xff0c;为了使用公式把x消去&#xff0c;令t2x&#xff0c;跟换区间 2、因为三角函数的几何性质&#xff0c;即sinx在0到Π上时对称区间&#xff0c;所以可以只计算[ 0 , Π/2 ]上的面积&#xff0c;最后乘2即可。 注&#xff1a;换元后记…

【服务器】Redis的安装及使用命令(Linux、Windows版)

目录 一、Redis简介 二、Redis安装 1、Linux版 1.1、下载 1.2、导入 1.3、解压 1.4、安装 1.5、修改文件 1.6、启动redis 1.7、测试 1.8、结束进程 1.9、修改密码访问 1.10、安装客户端工具&连接 2、Windows版 2.1、下载 2.2、安装 2.3、修改 2.4、连接 …

卷麻了,00后测试用例写的比我还好,简直无地自容......

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 如何编写测试用例&#xff1f; 作为一个测试新人&#xff0c;刚开始接触测试&#xff0c;对于怎么写测试用例很是头疼&#xff0c;无法…

【从删库到跑路】详解MySQL数据库的视图以及相关操作

&#x1f38a;专栏【MySQL】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【如愿】 &#x1f970;欢迎并且感谢大家指出小吉的问题 文章目录 &#x1f384;视图介绍&#x1f384;视图特点&#x1f33a;基本操作⭐创建视图⭐查询…

MIT6.5830 Lab1-GoDB实验记录(二)

MIT6.5830 Lab1-GoDB实验记录&#xff08;二&#xff09; – WhiteNights Site 标签&#xff1a;Golang, 数据库 接下来我们将完成tuple.go的缺失代码&#xff0c;并通过tuple_test.go的测试。 实验步骤 观察tuple.go 观察肯定是第一步&#xff0c;先打开tuple.go。 快300行代…

requires SDK version >=3.0.1 <4.0.0, version solving failed

这个很明显是FLUTTER SDK不匹配的问题&#xff0c;需要更新flutter SDK&#xff0c;最简单的办法&#xff0c;在flutter官网的页面直接下载最新的&#xff0c;然后替换之前旧版本的flutter 官网&#xff1a; 在 Windows 操作系统上安装和配置 Flutter 开发环境 - Flutter 中文…

基于GPIO子系统的LED驱动程序

这两个系统属于软件层&#xff0c;让我们不用直接对硬件配置&#xff0c;一般由芯片出产商写好&#xff0c;我们只要基于它们改就行。 设备树操作 1.使用官方的工具来编写pinctrl设置gpio的设备树代码。 如图&#xff0c;生成代码 ![在这里插入图片描述](https://img-blog.cs…

比亚迪今年的薪资。。

大家或许已经对比亚迪在西安的宣讲会有所耳闻&#xff0c;那场面真的是座无虚席。如果你稍微迟到了一些&#xff0c;那么你可能只能在门外或是走廊听了。 事实上&#xff0c;许多人早早地抵达了&#xff0c;只要稍微晚到&#xff0c;就可能错过了室内的位置。 更令人震惊的是&…

HCIA数据通信——静态路由

之前的文章中我提到过静态路由&#xff1a; 数据通信——网络层&#xff08;路由器以及数据转发流程&#xff09;_路由器如何转发数据_咕噜跳的博客-CSDN博客这里只做一些简单描述。 路由器关注的是网络之间的通信。路由器以自身为中心&#xff0c;考虑的是如何将数据发送到目…

优维产品最佳实践第13期:如何避免拨测机自身网络问题?

受限于拨测节点自身的环境&#xff0c;单一节点的拨测结果可能并不能反映出监控实例的真实运行状态 本期EasyOps产品使用最佳实践&#xff0c;我们将为您揭晓&#xff1a; 如何基于多点决策配置拨测监控&#xff0c;以避免拨测机自身网络问题而误告警&#xff1f; 如何对指标…

Paddle炼丹炉炸了Unexpected BUS error encountered in DataLoader worker

Paddle训练报错&#xff0c;内存不足 python train.py -c config/ResNet_W18.yaml修改配置文件config/ResNet_W18.yaml # 原配置 loader:num_workers: 4use_shared_memory: True# 修改后 loader:num_workers: 2use_shared_memory: False