JUC并发编程19 | 读写锁
有一些关于锁的面试题:
- 你知道 Java 里面有哪些锁?
- 读写锁的饥饿问题是什么?
- 有没有比读写锁更快的锁?
- StampedLock知道嘛?(邮戳锁/票据锁)
- ReentrantReadWriteLock 有锁降级机制?
ReentrantReadWriteLock
与 ReentrantLock 相比,ReentrantLock 实现了 Lock 接口,ReentrantReadWriteLock 实现了 ReadWriteLock 接口。对于ReentrantLock 它的 读读也是一个线程访问,浪费资源。ReentrantReadWriteLock 可以实现读读共享!
读写锁定义为:一个资源能被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程(读写互斥,读读共享)
class MyRescource { // 资源类模拟缓存Map<String, String> map = new HashMap<>();// ========= ReentrantLock 等价于 ======== SynchronizedLock lock = new ReentrantLock();// ========= ReentrantReadWritLock =======读读共享ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void write(String key, String value) {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t正在写入。。。。");map.put(key,value);try {TimeUnit.MILLISECONDS.sleep(1000);System.out.println(Thread.currentThread().getName()+"\t完成写入。。。。");}catch (InterruptedException e){e.printStackTrace();}}finally {lock.unlock();}}public void readWriteWrite(String key, String value) {readWriteLock.writeLock().lock();try{System.out.println(Thread.currentThread().getName()+"\t正在写入。。。。");map.put(key,value);try {TimeUnit.MILLISECONDS.sleep(1000);System.out.println(Thread.currentThread().getName()+"\t完成写入。。。。");}catch (InterruptedException e){e.printStackTrace();}}finally {readWriteLock.writeLock().unlock();}}public void read(String key) {lock.lock();try{System.out.println(Thread.currentThread().getName()+"\t正在读入。。。。");String s = map.get(key);try {// 1. 暂停500毫秒// 2. 暂停2000毫秒,显式读锁没有完成之前,写锁无法获取锁TimeUnit.MILLISECONDS.sleep(2000);System.out.println(Thread.currentThread().getName()+"\t完成读入。。。。\t"+s);}catch (InterruptedException e){e.printStackTrace();}}finally {lock.unlock();}}public void readWriteRead(String key) {readWriteLock.readLock().lock();try{System.out.println(Thread.currentThread().getName()+"\t正在读入。。。。");String s = map.get(key);try {// 1. 暂停500毫秒// 2. 暂停2000毫秒,显式读锁没有完成之前,写锁无法获取锁TimeUnit.MILLISECONDS.sleep(2000);System.out.println(Thread.currentThread().getName()+"\t完成读入。。。。\t"+s);}catch (InterruptedException e){e.printStackTrace();}}finally {readWriteLock.readLock().unlock();}}
}public class ReentrantReadWriteLockDemo {public static void main(String[] args) {MyRescource myRescource = new MyRescource();for (int i = 0; i < 10; i++) {int finalI = i;new Thread(()->{// myRescource.write("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));myRescource.readWriteWrite("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));},String.valueOf(i)).start();}for (int i = 0; i < 10; i++) {int finalI = i;new Thread(()->{// myRescource.read("key"+String.valueOf(finalI));myRescource.readWriteRead("key"+String.valueOf(finalI));},String.valueOf(i)).start();}try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}// 显式读锁没有完成之前,写锁无法获取锁for (int i = 0; i < 3; i++) {int finalI = i;new Thread(()->{// myRescource.write("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));myRescource.readWriteWrite("key"+String.valueOf(finalI),"value"+String.valueOf(finalI));},"新读写锁"+String.valueOf(i)).start();}}
}
锁降级
ReentrantReadWriteLock锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),锁的严苛程度变强叫做升级,反之叫做降级。
重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁。
锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁
写锁的降级,降级成为了读锁
- 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
- 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
- 如果释放了写锁,那么就完全转换为读锁。
保证数据的一致性
/**
所降级遵循了获取写锁,在获取读锁,再释放写锁的次序,写锁能够降级为读锁。因为写的优先级比读高
如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁
读没有完成时候写锁无法获得锁,只有在读锁读完才可以
*/
public void lockLevelDown(){ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();writeLock.lock();System.out.println("写入");readLock.lock();System.out.println("读入");writeLock.unlock();readLock.unlock();readLock.lock();System.out.println("读入");writeLock.lock();System.out.println("写入");readLock.unlock();writeLock.unlock();
}
/**
写入
读入
读入
*/
分析StampedLock,会发现它改进之处在于:
读的过程中也允许获取写锁介入(相当牛B,读和写两个操作也让你“共享”(注意引亏)),这样会守致戎们实的数掂趴可能个一以所以,需要额外的方法来判断读的过程中是否有写入,这是一种乐观的读锁。
显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。
为什么要锁降级?
锁降级的必要性1:
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
部分人读完上述话可能有些疑惑,针对上面黑体字那句话,为什么无法感知线程T的数据更新?我当前线程再次获取读锁的时候不是可以察觉到数据在主存中的变化吗? 我参考了一些资料,对该 “”数据可见性“” 有了另一种理解,理解是 当前线程为了保证数据的可见性,这是指线程自己更改了数据,自己应该要察觉到数据的变化,如果没有读锁,更改完数据之后线程T获取到了写锁并更改了数据,则当前线程读到的数据是线程T更改的,并不是自己更改的,当前线程并不知道是线程T修改了自己要读的(原来自己改的)数据,所以可能导致当前线程在执行后续代码的时候结果出错,这时就导致了数据的不可见,即当前线程并无法察觉到自己修改的值!
锁降级的必要性2:
为了提高程序执行性能,可能存在一个事务线程不希望自己的操作被别的线程中断,而这个事务操作可能分成多部分操作更新不同的数据(或表)甚至非常耗时。如果长时间用写锁独占,显然对于某些高响应的应用是不允许的,所以在完成部分写操作后,退而使用读锁降级,来允许响应其他进程的读操作。只有当全部事务完成后才真正释放锁。
StampedLock
邮戳锁、版本锁是比读写锁更快的锁。
StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中读写锁ReentrantReadWriteLock的优化。
stamp(戳记,long 类型):代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值。
锁饥饿问题:ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。
如何缓解锁饥饿问题:
- 使用“公平”策略可以一定程度上缓解这个问题
new ReentrantReadWriteLock(true) - 但是“公平"策略是以牺牲系统吞吐量为代价
StampedLock 类的乐观读锁:对于短的制度代码段,使用乐观模式通常可以减少争用并提高吞吐量
ReentrantReadWriteLock
允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,
读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,
原因就是在于ReentrantReadWriteLock支持读并发,读读可以共享
StampedLock横空出世
ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,
所以,在获取乐观读锁后,还需要对结果进行校验。
StampedLock的特点
所有获得锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
StampedLock 是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock 有三种访问模式:
- Reading(读模式悲观):功能和ReentrantReadWriteLock 的读锁类似
- Writeing(写模式):功能和ReentrantReadWriteLock 的写锁类似
- Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式——读的过程中也允许获取写锁介入
邮戳读写锁的传统版本
public class StampedLockDemo {static int number = 37;static StampedLock stampedLock = new StampedLock();public void write(){long stamp = stampedLock.writeLock();System.out.println(Thread.currentThread().getName()+"\t 写线程准备修改。");try{number = number + 13;} finally {stampedLock.unlockWrite(stamp);}System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改。");}public void read(){long stamp = stampedLock.readLock();System.out.println(Thread.currentThread().getName()+"\t 悲观读线程准备修改。..");try{for (int i = 0; i < 4; i++) {TimeUnit.SECONDS.sleep(1);System.out.println("读线程正在读取中");}int result = number;System.out.println(Thread.currentThread().getName()+"\t 悲观读线程结束修改。result = "+result);System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥");} catch (InterruptedException e) {e.printStackTrace();} finally {stampedLock.unlockRead(stamp);}}public static void main(String[] args) {StampedLockDemo demo = new StampedLockDemo();new Thread(()->{demo.read();},"读线程").start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{demo.write();},"写线程").start();}}
/**
读线程 悲观读线程准备修改。..
读线程正在读取中
读线程正在读取中
读线程正在读取中
读线程正在读取中
读线程 悲观读线程结束修改。result = 37
写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥
写线程 写线程准备修改。
写线程 写线程结束修改。
*/
邮戳锁乐观版本
public class StampedLockDemo {static int number = 37;static StampedLock stampedLock = new StampedLock();public void write(){long stamp = stampedLock.writeLock();System.out.println(Thread.currentThread().getName()+"\t 写线程准备修改。");try{number = number + 13;} finally {stampedLock.unlockWrite(stamp);}System.out.println(Thread.currentThread().getName()+"\t 写线程结束修改。");}public void read(){long stamp = stampedLock.readLock();System.out.println(Thread.currentThread().getName()+"\t 悲观读线程准备修改。..");try{for (int i = 0; i < 4; i++) {TimeUnit.SECONDS.sleep(1);System.out.println("读线程正在读取中");}int result = number;System.out.println(Thread.currentThread().getName()+"\t 悲观读线程结束修改。result = "+result);System.out.println("写线程没有修改成功,读锁时候写锁无法介入,传统读写互斥");} catch (InterruptedException e) {e.printStackTrace();} finally {stampedLock.unlockRead(stamp);}}public void tryOptimisticRead(){long stamp = stampedLock.tryOptimisticRead();int result = number;System.out.println("4秒前stampedLock.validate方法值 (true无修改,false有修改)\t"+stampedLock.validate(stamp));for (int i = 0; i < 4; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+"\t正在读取"+i+"秒,后stampedLock.validate方法值\t"+stampedLock.validate(stamp));}if (!stampedLock.validate(stamp)){System.out.println("有人修改------有写操作");stamp = stampedLock.readLock();try{System.out.println("从乐观读 升级为 悲观读");result = number;System.out.println("从新悲观读后result="+result);}finally {stampedLock.unlockRead(stamp);}}System.out.println(Thread.currentThread().getName()+"\t最后的值是"+result);}public static void main(String[] args) {StampedLockDemo demo = new StampedLockDemo();new Thread(()->{System.out.println(Thread.currentThread().getName()+"进入");demo.tryOptimisticRead();},"乐观读线程").start();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}new Thread(()->{System.out.println(Thread.currentThread().getName()+"进入");demo.write();},"写线程").start();}
}
/**
乐观读线程进入
4秒前stampedLock.validate方法值 (true无修改,false有修改) true
乐观读线程 正在读取0秒,后stampedLock.validate方法值 true
写线程进入
写线程 写线程准备修改。
写线程 写线程结束修改。
乐观读线程 正在读取1秒,后stampedLock.validate方法值 false
乐观读线程 正在读取2秒,后stampedLock.validate方法值 false
乐观读线程 正在读取3秒,后stampedLock.validate方法值 false
有人修改------有写操作
从乐观读 升级为 悲观读
从新悲观读后result=50
乐观读线程 最后的值是50
*/
邮戳锁的缺点:
- StampedLock 不支持重入,没有 Re 开头
- StampedLock 的悲观读锁和写作都不支持条件变量
- 使用StampedLock一定不要调用中断操作
相关文章:
JUC并发编程19 | 读写锁
有一些关于锁的面试题: 你知道 Java 里面有哪些锁?读写锁的饥饿问题是什么?有没有比读写锁更快的锁?StampedLock知道嘛?(邮戳锁/票据锁)ReentrantReadWriteLock 有锁降级机制? Ree…...
springboot_maven项目怎么引入mybatis
在pom.xml文件中添加mybatis和mybatis-spring-boot-starter的依赖 org.mybatis mybatis ${mybatis.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.spring.version} 配置mybatis 在application.properties(或application.yml࿰…...
JAVA8的新特性——lambda表达式
JAVA8的新特性——lambda表达式 此处,我们首先对于Java8的一些特性作为一个简单介绍 Java 8是Java编程语言的一个重要版本,于2014年发布。Java 8引入了许多新特性和改进,以提高开发效率和性能。以下是Java 8的一些主要新特性: Lam…...
算法修炼之练气篇——练气六层
博主:命运之光 专栏:算法修炼之练气篇 前言:每天练习五道题,炼气篇大概会练习200道题左右,题目有C语言网上的题,也有洛谷上面的题,题目简单适合新手入门。(代码都是命运之光自己写的…...
利用GPU并行计算beta-NTI,大幅减少群落构建计算时间
1 先说效果 18个样本,抽平到8500条序列,4344个OTUs,计算beta-NTI共花费时间如下。如果更好的显卡,更大的数据量,节约的时间应该更加可观。 GPU(GTX1050):1分20秒 iCAMP包 的bNTIn.p(…...
Shiro框架漏洞分析与复现
Shiro简介 Apache Shiro是一款开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性,可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。 1、Shiro反序列…...
(数字图像处理MATLAB+Python)第七章图像锐化-第一、二节:图像锐化概述和微分算子
文章目录 一:图像边缘分析二:一阶微分算子(1)梯度算子A:定义B:边缘检测C:示例D:程序 (2)Robert算子A:定义B:示例C:程序 &a…...
C# | 内存池
内存池 文章目录 内存池前言什么是内存池内存池的优点内存池的缺点 实现思路示例代码结束语 前言 在上一篇文章中,我们介绍了对象池的概念和实现方式。对象池通过重复利用对象,避免了频繁地创建和销毁对象,提高了系统的性能和稳定性。 今天我…...
程序设计入门——C语言2023年5月10日
程序设计入门——C语言 1、window下安装gcc 课程来源:链接: 浙江大学 翁恺 程序设计入门——C语言 学习日期:2023年5月10日 1、window下安装gcc 如果想让gcc在windows下运行,需要将gcc,及对于的lib包,都安装到window…...
【2023华为OD笔试必会25题--C语言版】《03 单入口空闲区域》——递归、数组、DFS
本专栏收录了华为OD 2022 Q4和2023Q1笔试题目,100分类别中的出现频率最高(至少出现100次)的25道,每篇文章包括原始题目 和 我亲自编写并在Visual Studio中运行成功的C语言代码。 仅供参考、启发使用,切不可照搬、照抄,查重倒是可以过,但后面的技术面试还是会暴露的。✨✨…...
Grafana安装、升级与备份(02)
一、安装Grafana软件包 Grafana部署非常简单,直接使用yum命令从官网拉到安装再启动就可以了,本次使用的grafana版本为9.5.0 官网下载地址:Download Grafana | Grafana Labs # wget yum install -y https://dl.grafana.com/oss/release/grafana-9.5.0-1.x86_64.rpm # yum …...
【2023华为OD笔试必会25题--C语言版】《10 相同数字的积木游戏》——数组
本专栏收录了华为OD 2022 Q4和2023Q1笔试题目,100分类别中的出现频率最高(至少出现100次)的25道,每篇文章包括原始题目 和 我亲自编写并在Visual Studio中运行成功的C语言代码。 仅供参考、启发使用,切不可照搬、照抄,查重倒是可以过,但后面的技术面试还是会暴露的。✨✨…...
awk命令编辑
awk工作原理 逐行读取文本,默认以空格或tab键分隔符进行分隔,将分隔所得的各个字段保存到内建变量中,并按模式或者条件执行编辑命令。 sed命令常用于一整行的处理,而awk比较倾向于将一行分成多个“字段”然后再进行处理。awk信息…...
Pinia和Vuex的区别
Pinia和Vuex都是Vue.js状态管理库 Pinia是一个轻量级的状态管理库,它专注于提供一个简单的API来管理应用程序的状态。 相比之下,Vuex是一个更完整的状态管理库,它提供了更多的功能,比如模块化、插件和严格模式等。 Pinia是基于V…...
《C++高并发服务器笔记——第四章Linux网络编程》
计算机网络等相关知识可以去小林coding进行巩固(点击前往) 《C高并发服务器笔记——第四章》 4.1、网络结构模式1.C/S结构①C/S结构简介②C/S结构优点③C/S结构缺点 2.B/S结构①B/S结构简介②B/S结构优点③B/S结构缺点 4.2和4.3、MAC地址、IP地址、端口…...
NFS服务器搭建(案例)
目录标题 第一个问题1.安装软件包2.进入配置文件进行定义,并创建对应的资源文件3.客户端进行挂载,并查看挂载信息,修改挂载权限4.客户端查看挂载的信息 第二个问题1.服务端配置文件进行定义,并创建对应资源文件2.客户端进行挂载3.…...
ubuntu 22.04 安装 Docker Desktop 及docker介绍
目录 一、Docker Desktop 安装 1、我们先去官网下载安装包 2、Install Docker Desktop on Ubuntu 3、Launch Docker Desktop 二、Docker 介绍 什么是docker 如何使用docker docker是如何工作的 docker build docker run docker pull 一、Docker Desktop 安装 1、我们先…...
微前端中的应用隔离是什么,一般是怎么实现的?
微前端中的应用隔离是什么,一般是怎么实现的? 前言一、iframe 隔离二、Web Components三、JavaScript 沙箱隔离四、Shadow DOM 隔离总结 前言 微前端中的应用隔离是指将不同的微前端应用程序隔离开来,以确保它们之间不会相互影响或干扰。这种隔离可以通…...
【python pandas】合并文件并剔除重复数据
1.背景 工作中需要处理多个文件,每个文件里面有重复的数据,剔除重复数据,保留最新的数据 2.代码: import pandas as pd import osdl [] #person_list是文件路径 for i in range(person_list_len):#把文件df全部集合进列表dldl.a…...
Spellman高压电源X射线发生器维修XRB160PN480X4593
spellman高压发生器维修VMX40P5X4629;Spellman X射线发生器维修X4593系列 X射线源维修。 Spellman所拥有的变频器架构可以使高压电源获得高利用率的效率和功率密度。固体密封的高压模块进一步减少了尺寸和重量。 基于表面贴装控制电路的数字信号处理器提供通讯接口…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
第 86 场周赛:矩阵中的幻方、钥匙和房间、将数组拆分成斐波那契序列、猜猜这个单词
Q1、[中等] 矩阵中的幻方 1、题目描述 3 x 3 的幻方是一个填充有 从 1 到 9 的不同数字的 3 x 3 矩阵,其中每行,每列以及两条对角线上的各数之和都相等。 给定一个由整数组成的row x col 的 grid,其中有多少个 3 3 的 “幻方” 子矩阵&am…...
python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
