ReentrantLock底层原理学习二

以 ReentrantLock 作为切入点,来看看在这个场景中是如何使用 AQS 来实现线程的同步的

ReentrantLock 的时序图

调用 ReentrantLock 中的 lock()方法,源码的调用过程我使用了时序图来展现。

在这里插入图片描述
ReentrantLock.lock()
这个是 reentrantLock 获取锁的入口

public void lock() {sync.lock();
}
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能
Sync 有两个具体的实现类,分别是:
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
FailSync: 表示所有线程严格按照 FIFO 来获取锁
NofairSync.lock 
以非公平锁为例,来看看 lock 中的实现
1. 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有没有线程排队,我先上来 cas 去抢占一下
2. CAS 成功,就表示成功获得了锁
3. CAS 失败,调用 acquire(1)走锁竞争逻辑
final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}
CAS 的实现原理
protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通过 cas 乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返回 false.
这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到 state 这个属性的意义。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态。它有两个含义的表示
1. 当 state=0 时,表示无锁状态
2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增,比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0其他线程才有资格获得锁。
Unsafe 类
Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;
stateOffset
一个 Java 对象可以看成是一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确地告诉你某个字段相对于对象的起始内存地址的字节偏移。用于在后面的 compareAndSwapInt 中,去根据偏移量找到对象在内存中的具体位置,所以 stateOffset 表示 state 这个字段在 AQS 类的内存中相对于该类首地址的偏移量。
compareAndSwapInt
在 unsafe.cpp 文件中,可以找到 compareAndSwarpInt 的实现
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj); //将 Java 对象解析成 JVM 的 oop(普通对象指针),jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); //根据对象 p 和地址偏移量找到地址return (jint)(Atomic::cmpxchg(x, addr, e)) == e; //基于 cas 比较并替换, x 表示需要更新的值,addr 表示 state
在内存中的地址,e 表示预期值
UNSAFE_END
AQS.accquire 
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此时继续 acquire(1)操作
这个方法的主要逻辑是
1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加到 AQS 队列尾部
3. acquireQueued,将 Node 作为参数,通过自旋去尝试获取锁。
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
NonfairSync.tryAcquire
这个方法的作用是尝试获取锁,如果成功返回 true,不成功返回 false它是重写 AQS 类中的 tryAcquire 方法,并且大家仔细看一下 AQS 中 tryAcquire方法的定义,并没有实现,而是抛出异常。按照一般的思维模式,既然是一个不实现的模版方法,那应该定义成 abstract,让子类来实现
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}
ReentrantLock.nofairTryAcquire
1. 获取当前线程,判断当前的锁的状态
2. 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值
3. 当前线程是属于重入,则增加重入次数
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//获取当前执行的线程int c = getState();//获得 state 的值if (c == 0) {//表示无锁状态if (compareAndSetState(0, acquires)) {//cas 替换 state 的值,cas 成功表示获取锁成功setExclusiveOwnerThread(current);//保存当前获得锁的线程,下次再来的时候不要再尝试竞争锁return true;}}else if (current == getExclusiveOwnerThread()) {//如果同一个线程来获得锁,直接增加重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
AQS.addWaiter
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成Node.入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状态。意味着重入锁用到了 AQS 的独占锁功能
1. 将当前线程封装成 Node
2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的node 添加到 AQS 队列
3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);//把当前线程封装为 NodeNode pred = tail; //tail 是 AQS 中表示同比队列队尾的属性,默认是 nullif (pred != null) {//tail 不为空的情况下,说明队列中存在节点node.prev = pred;//把当前线程的 Node 的 prev 指向 tailif (compareAndSetTail(pred, node)) {//通过 cas 把 node加入到 AQS 队列,也就是设置为 tailpred.next = node;//设置成功以后,把原 tail 节点的 next指向当前 nodereturn node;}}enq(node);//tail=null,把 node 添加到同步队列return node;
}
enq
enq 就是通过自旋操作把当前节点加入到队列中
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}
AQS.acquireQueued 
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给acquireQueued 方法,去竞争锁
1. 获取当前节点的 prev 节点
2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
5. 最后,通过 cancelAcquire 取消获得锁的操作
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//获取当前节点的 prev 节点if (p == head && tryAcquire(arg)) {//如果是 head 节点,说明有资格去争抢锁setHead(node);//获取锁成功,也就是ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权限p.next = null; //把原 head 节点从链表中移除failed = false;return interrupted;}//ThreadA 可能还没释放锁,使得 ThreadB 在执行 tryAcquire 时会返回 falseif (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true; //并且返回当前线程在等待过程中有没有中断过。}} finally {if (failed)cancelAcquire(node);}
}
NofairSync.tryAcquire
这个方法在前面分析过,就是通过 state 的状态来判断是否处于无锁状态,然后在通过 cas 进行竞争锁操作。成功表示获得锁,失败表示获得锁失败
shouldParkAfterFailedAcquire
如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,进入该状态后的结点将不会再变化
SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
CONDITION: 和 Condition 有关系,后续会讲解
PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态
0:初始状态
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是否应该被挂起。
1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
2. 通过循环扫描链表把 CANCELLED 状态的节点移除
3. 修改 pred 节点的状态为 SIGNAL,返回 false.
返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//前置节点的waitStatusif (ws == Node.SIGNAL)//如果前置节点为 SIGNAL,意味着只需要等待其他前置节点的线程被释放,return true;//返回 true,意味着可以直接放心的挂起了if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排队,直接移除这个节点就行do {node.prev = pred = pred.prev;//相当于: pred=pred.prev; node.prev=pred;} while (pred.waitStatus > 0); //这里采用循环,从双向列表中移除 CANCELLED 的节点pred.next = node;} else {//利用 cas 设置 prev 节点的状态为 SIGNAL(-1)compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
parkAndCheckInterrupt
使用 LockSupport.park 挂起当前线程编程 WATING 状态Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是

thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}
selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的
static void selfInterrupt() {Thread.currentThread().interrupt();
}
图解分析
通过 acquireQueued 方法来竞争锁,如果 ThreadA 还在执行中没有释放锁的话,意味着 ThreadB 和 ThreadC 只能挂起了。

在这里插入图片描述
LockSupport
LockSupport类是 Java6引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了 Unsafe 类里的函数,归结到 Unsafe 里,只有两个函数unpark 函数为线程提供“许可(permit)”,线程调用 park 函数则等待“许可”。这个有
点像信号量,但是这个“许可”是不能叠加的,“许可”是一次性的。permit 相当于 0/1 的开关,默认是 0,调用一次 unpark 就加 1 变成了 1.调用一次park 会消费 permit,又会变成 0。 如果再调用一次 park 会阻塞,因为 permit 已经是 0 了。直到 permit 变成 1.这时调用 unpark 会把 permit 设置为 1.每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会累积
锁的释放流程
如果这个时候 ThreadA 释放锁了,那么我们来看锁被释放后会产生什么效果
ReentrantLock.unlock
在 unlock 中,会调用 release 方法来释放锁

public final boolean release(int arg) {if (tryRelease(arg)) { //释放锁成功Node h = head; //得到 aqs 中 head 节点if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点unparkSuccessor(h);return true;}return false;
}
ReentrantLock.tryRelease 
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。
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;
}

unparkSuccessor

private void unparkSuccessor(Node node) {int ws = node.waitStatus;//获得 head 节点的状态if (ws < 0)compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点状态为 0Node s = node.next;//得到 head 节点的下一个节点if (s == null || s.waitStatus > 0) {//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.//通过从尾部节点开始扫描,找到距离 head 最近的一个waitStatus<=0 的节点s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) //next 节点不为空,直接唤醒这个线程即可LockSupport.unpark(s.thread);
}
1. 将新的节点的 prev 指向 tail
2. 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
3. t.next=node;设置原 tail 的 next 节点指向新的节点
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

在这里插入图片描述
在 cas 操作之后,t.next=node 操作之前。存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。
图解分析
通过锁的释放,原本的结构就发生了一些变化。head 节点的 waitStatus 变成了 0,ThreadB 被唤醒。
在这里插入图片描述
原本挂起的线程继续执行
通过 ReentrantLock.unlock,原本挂起的线程被唤醒以后继续执行,应该从哪里执行大家还有印象吧。 原来被挂起的线程是在 acquireQueued 方法中,所以被唤醒以后继续从这个方法开始执行。
AQS.acquireQueued
这个方法前面已经完整分析过了,我们只关注一下 ThreadB 被唤醒以后的执行流程。
由于 ThreadB 的 prev 节点指向的是 head,并且 ThreadA 已经释放了锁。所以这个时候调用 tryAcquire 方法时,可以顺利获取到锁。
1. 把 ThreadB 节点当成 head
2. 把原 head 节点的 next 节点指向为 null

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}
图解分析 
1. 设置新 head 节点的 prev=null
2. 设置原 head 节点的 next 节点为 null

在这里插入图片描述
公平锁和非公平锁的区别
锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点
有两个
FairSync.tryAcquire

final void lock() {acquire(1);
}

非公平锁在获取锁的时候,会先通过 CAS 进行抢占,而公平锁则不会
FairSync.tryAcquire

protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
这个方法与 nonfairTryAcquire(int acquires)比较,不同的地方在于判断条件多了hasQueuedPredecessors()方法,也就是加入了[同步队列中当前节点是否有前驱节点]的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁

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

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

相关文章

PPT模板(100套IT科技互联网风)

哈喽&#xff0c;小伙伴们&#xff0c;最近是不是都在准备年终总结、年终述职&#xff0c;一个好的PPT模板是编写报告的开端。我最近也在准备年终总结报告&#xff0c;一块整理了一些PPT模板。这些模板适用于各种IT科技互联网相关的场合&#xff0c;如产品发布会、项目提案、工…

案例081:基于微信小程序的移动平台的远程在线诊疗系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;SSM JDK版本&#xff1a;JDK1.8 数据库&#xff1a;mysql 5.7 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.5.4 小程序框架&#xff1a;uniapp 小程序开发软件&#xff1a;HBuilder X 小程序…

MybatisPlus—自定义SQL

目录 1. 自定义SQL介绍 2. 自定义SQL使用步骤 3. 自定义SQL实例 4.总结 1. 自定义SQL介绍 介绍&#xff1a;自定义SQL并不是由我们来编写全部SQL语句&#xff0c;而是通过利用MyBatisPlus的Wrapper来构建复杂的Where条件&#xff0c;然后自己定义SQL语句中剩下的部分。 使…

【AI视野·今日CV 计算机视觉论文速览 第283期】Thu, 4 Jan 2024

AI视野今日CS.CV 计算机视觉论文速览 Thu, 4 Jan 2024 Totally 85 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers LEAP-VO: Long-term Effective Any Point Tracking for Visual Odometry Authors Weirong Chen, Le Chen, Rui Wang, Marc P…

13年测试老鸟,性能测试-全链路压测总结,一文打通...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、什么是全链路压…

2023湾区产城创新大会:培育数字化供应链金融新时代

2023年12月26日&#xff0c;由南方报业传媒集团指导&#xff0c;南方报业传媒集团深圳分社主办的“新质新力——2023湾区产城创新大会”在深圳举行。大会聚集里国内产城研究领域的专家学者以及来自产业园区、金融机构、企业的代表&#xff0c;以新兴产业发展为议题&#xff0c;…

Godot4.2——爬虫小游戏简单制作

目录 一、项目 二、项目功能 怪物 人物 快捷键 分数 游戏说明 提示信息 三、学习视频 UI制作 游戏教程 四、总结 一、项目 视频演示&#xff1a;Godot4爬虫小游戏简单制作_哔哩哔哩bilibili 游戏教程&#xff1a;【小猫godot4入门教程 C#版 已完结】官方入门案例 第…

云渲染有几种方式?适合设计师的渲染方式是哪种?

云渲染是很多公司首选的渲染方式&#xff0c;它能加快渲染速度提高工作效率&#xff0c;那么云渲染有几种渲染方式呢&#xff1f;这次我们一起来看看。 1、离线渲染 离线渲染也被称作预渲染&#xff0c;通常用于对真实感和复杂细节有高要求的场合&#xff0c;如电影、动画、特效…

深度学习数据集大合集—鱼类数据集

最近收集了一大波有关于各类鱼类的数据集&#xff0c;有淡水鱼、有深海鱼、有鱼的状态、有鱼的分类。大家可以详细查看。废话不多说&#xff0c;接下下来逐一的给大家介绍&#xff01;&#xff01; 1、鱼类检测数据集 包含鱼类的对象检测数据集 本数据集包含4种鱼类及其相关…

移动通信原理与关键技术学习(3)

1.什么是相干解调&#xff1f;什么是非相干解调&#xff1f;各自的优缺点是什么&#xff1f; 相干解调需要在接收端有一个与发送端一样的载波&#xff08;同样的频率和相位&#xff09;&#xff0c;在接收端的载波与发送端载波进行互相关操作&#xff0c;去除载波的影响。相干…

如何使用 CMake 生成一个静态库

文章目录 tutorial_3/CMakeLists.txttutorial_3/src/CMakeLists.txtcmake_tutorial/tutorial_3/src/hello.cpptutorial_3/src/hello.h根目录的 CMakeLists.txtsrc 目录的 CMakeLists.txthello.cpp 和 hello.h构建过程总结 tutorial_3/CMakeLists.txt cmake_minimum_required(V…

Latex + Overleaf 论文写作新手笔记

.tex 文件main.tex 文件 Latex 的文档层次结构不同文档类型的层次结构report 6 层结构实例article 5 层结构实例 Latex 语法图表插入与引用使用 figure 环境来插入图片使用 ref 命令来引用已有的图表格的插入与引用 代码块列表无序列表 itemize有序列表 enumerate 学位论文项目…

DQL命令查询数据(一)

本课目标 理解查询的相关概念 掌握MySQL的简单查询语句 掌握MySQL中的函数 DQL 语言 DQL&#xff08;Data Query Language 数据查询语言&#xff09;&#xff1a;用于查询数据库对象中所包含的数据 DQL语言主要的语句&#xff1a;SELECT语句 DQL语言是数据库语言中最核心…

CSS3(Flex布局详解)

Flex 基本概念&#xff1a; 在 flex 容器中默认存在两条轴&#xff0c;水平主轴(main axis) 和垂直的交叉轴(cross axis)&#xff0c;这是默认的设置&#xff0c;当然你可以通过修改使垂直方向变为主轴&#xff0c;水平方向变为交叉轴&#xff0c;这个我们后面再说。 在容器中…

UseContentHash选项能否在打包AssetBundle时计算可靠的Hash

1&#xff09;UseContentHash选项能否在打包AssetBundle时计算可靠的Hash 2&#xff09;如何清理Native Reserved部分的内存 3&#xff09;Addressables资源完整性校验 4&#xff09;通过Image.color和CanvasRenderer.SetColor修改UI组件颜色的区别 这是第368篇UWA技术知识分享…

[Linux] 一文理解HTTPS协议:什么是HTTPS协议、HTTPS协议如何加密数据、什么是CA证书(数字证书)...

之前的文章中, 已经分析介绍过了HTTP协议. HTTP协议在网络中是以明文的形式传输的. 无论是GET还是POST方法都是不安全的. 为什么不安全呢? 因为: HTTP协议以明文的形式传输数据, 缺乏对信息的保护. 如果在网络中传输数据以明文的形式传输, 网络中的任何人都可以轻松的获取数据…

Java学习苦旅(二十六)——反射,枚举和lamda表达式

本篇博客将讲解反射&#xff0c;枚举和lamda表达式。 文章目录 反射定义用途反射基本信息反射相关的类Class类Class类中相关的方法 反射示例反射的优缺点优点缺点 枚举背景及定义常用方法枚举优缺点优点缺点 Lambda表达式背景语法函数式接口定义基本使用 变量捕获Lambda在集合…

科学的摇篮 - 贝尔实验室

AT&T贝尔实验室&#xff08;AT&T Bell Laboratories&#xff09;是美国电信公司AT&T的研究与开发部门&#xff0c;成立于1925年。它在20世纪的许多年里一直是科学与技术创新的重要中心&#xff0c;做出了众多重大贡献&#xff0c;并为多项科技成就奠定了基础。以下…

Typescript 中创建对象的方式

1.type type MyObj {a: string;b: number;c: () > number; }; 2.interface interface MyObj {a: string;b: number;c: () > number; } 3. class class MyObj {a:string;b:number;c:()>number } // Error: Property staticProperty does not exist on type M.

Spring Boot应用启动时自动执行代码的五种方式

Spring Boot为开发者提供了多种方式在应用启动时执行自定义代码&#xff0c;这些方式包括注解、接口实现和事件监听器。在本篇博客中&#xff0c;我们将探讨一些常见的方法&#xff0c;以及如何利用它们在应用启动时执行初始化逻辑。 1. PostConstruct注解 PostConstruct注解…