深入了解java锁升级可以应对各种疑难问题

对于java锁升级,很多人都停留在比较浅层的表面理解,一定程度下也许够用,但如果学习其中的细节,我们更好地理解多线程并发时各种疑难问题的应对方式!

因此我将锁升级过程中可能涉及的大部分细节或者疑问都整合成了一篇文章,希望你能直接在这篇文章中,搞懂你当年学习这块时遗留的所有疑问。

为什么说线程切换会很慢?

所谓的用户态和内核态之间的切换仅仅是一方面, 并非全部, 更关键的在于, 多CPU的机器中,当你切换了线程,意味着线程在原先CPU上的缓存也许不再有用, 因为当线程重新触发时,可能在另一个CPU上执行了。

正因如此,不建议没事就挂起线程, 让线程自旋会比挂起后切换CPU好很多。

正与基于这一点,才有了后面的sync锁升级机制,理解了为什么要锁升级,才能逐步理解锁升级过程,

对象头中的mark-word

java每个对象的对象头中, 都有32或者64位的mark-word。

mark-word是理解锁升级过程的重要部分,且后面的锁升级过程都会涉及,因此这里会进行一个非常详细的解释。这部分只对一个对象必有的属性做解释(即一般不会随着锁状态变化而消失的属性)。对于各锁状态独有的属性,会在锁升级过程中做详细的解释。

image.png

锁状态标志位 +偏向锁标记位(2bit + 1bit)

除了markword中的2位锁状态标志位, 其他62位都会随着锁状态标志位的变化而变化。

这里先列出各锁状态标志位代表的当前对象所用锁的情况。后面会详细解释各种锁的含义和运行原理。

  • 锁状态标志位为01: 属于无锁或者偏向锁状态。因此还需要额外的偏向锁标记位1bit来确认是无锁还是偏向锁
  • 锁状态标志位为00: 轻量级锁
  • 锁状态标志位为10: 重量级锁
  • 锁状态标志位为11: 已经被gc标记,即将释放

为什么无锁/偏向锁的标志位是01,而轻量级锁的标志位是00?

即按理说,无锁是锁状态的初始情况,为什么标志位不是从00开始?

个人查询到的一个解释,是因为 轻量级锁除了锁标志位外,另外62位都是一个指针地址。

如果将轻量级锁标志位设置为00, 那么在判断标志位为00后, m无需再额外做一次markWord>>2的操作,而是直接将markWord拿来当作地址使用即可!

可以从这里看到jvm的设计者还是非常细节的,并没有随意地定义各状态的标志位

hashcode(31bit)

哈希code很容易理解,将对象存储到一些map或者set里时,都需要hashcode来确认插入位置。

但markword里的hashcode,和我们平时经常覆写的hashCode()还是有区别的。

markword中的hashcode是哪个方法生成的?

很多人误以为,markword中的hashcode是由我们经常覆写的hashcode()方法生成的。

实际上, markword中的hashcode只由底层 JDK C++ 源码计算得到(java侧调用方法为 System.identityHashCode() ), 生成后固化到markword中,

如果你覆写了hashcode()方法, 那么每次都会重新调用hashCode()方法重新计算哈希值。

根本原因是因为你覆写hashcode()之后,该方法中很可能会利用被修改的成员来计算哈希值,所以jvm不敢将其存储到markword中。

因此,如果覆写了hashcode()方法,对象头中就不会生成hashcode,而是每次通过hashcode()方法调用

markword中的hashcode是什么时候生成?

很容易误以为会是对象一创建就生成了。

实际上,是采用了延迟加载技术,只有在用到的时候才生成。

毕竟有可能对象创建出来使用时,并不需要做哈希的操作。

hashcode在其他锁状态中去哪了?

这个问题会在后面锁升级的3个阶段中,解释hashcode的去向。其他的例如分代年龄同理。

gc分代年龄(4bit)

在jvm垃圾收集机制中, 决定年轻代什么时候进入老年代的根据之一, 就是确认他的分代年龄是否达到阈值,如下图所示。

image.png

分代年龄只有4bit可以看出,最大值只能是15。因此我们设置的进入老年代年龄阈值 -XX:MaxTenuringThreshold 最大只能设置15。

cms_free

在无锁和偏向锁中,还可以看到有1bit的cms_free。

实际上就是只有CMS收集器用到的。但最新java11中更多用的是G1收集器了,这一位相当于不怎么常用,因此提到的也非常少。

从上述可以看出, 只有锁状态标记位、 hashcode、 分代年龄、cms_free是必有的, 但是从markword最初的示意图来看, hashcode、 分代年龄、cms_free似乎并非一直存在,那么他们去哪了呢?会在后面的锁升级过程进行详细解释。

锁升级四个阶段超级详解

无锁

无锁状态的markword如下所示,可以看到上文提到的信息都存在

image.png

处于无锁状态的条件或者时机是什么?

无锁状态用于对象刚创建,且还未进入过同步代码块的时候

这一点很重要, 意味着如果你没有同步代码块或者同步方法, 那么将是无锁状态。

对象从没进入同步块,为什么偏向锁标志位却是1?

上面这个问题说过,没进入同步块, 不会上偏向锁。

但是我们如果用java的jol工具测试打印新对象,会看到低3位是101

image.png

这其实是jvm后面加入的一种优化, 对每个新对象,预置了一个**“可偏向状态”,也叫做匿名偏向状态**,是对象初始化中,JVM 帮我们做的。

注意此时 markword中高位是不存在ThreadID的, 都是0, 说明此时并没有线程偏向发生,因此也可以理解成是无锁。

好处在于后续做偏向锁加锁时,无需再去改动偏向锁标记位,只需要对线程id做cas即可。

偏向锁

一旦代码第一次进入sync同步方法块,就可能从无锁状态进入偏向锁状态。

另外很多人应该都知道, 偏向锁只存储了当前偏向的线程id, 只有线程id不同的才会触发升级。

但这是非常简化的说法, 实际上中间的细节和优化非常之多!这里将为你详细讲述。

为什么要有偏向锁?

理解这个才能理解偏向锁中的各种设计。 假设我们new出来的对象带有同步代码块方法,但在整个生命周期中只被一个线程访问,那么是否有必要做消耗消耗的竞争动作,甚至引入额外的内存开销?没有必要。

因此针对的是 对象有同步方法调用,但是实际不存在竞争的场景

偏向锁的markword详解

image.png

这个markword和无锁对比, 偏向标志位变成了1, hashcode没了,多了个epoch和线程id。

markword中的当前线程id

这个id就是在进入了对象同步代码块的线程id。

java的线程id是一个long类型, 按理说是64位,但为什么之类的线程id只有54位?

具体没有找到解释,可能是jvm团队认为54位线程id足够用了,不至于会创建2^54那么多的线程,真的有需要创建这么频繁的程序,也会优先采用线程池池才对

线程id如何写入?

线程id是直接写入markword吗? 不对, 一定要注意到这时候是存在同时写的可能的。

因此会采用CAS的方式进行线程id的写入。 简而言之, 就是先取原线程id后,再更新线程id,更新后检查一下是否和预期一致,不一致则说明被人改动过,则线程id写入失败,说明存在竞争,升级为轻量级锁。

哈希code去哪了

我们注意到无锁时的hashcode不见了。

对于偏向锁而言, 一旦在对象头中设置过hashcode, 那么进入同步块时就不会进入偏向锁状态,会直接跳到轻量级锁,毕竟偏向锁里没有存放hashcode的地方(下文的轻量级锁和重量级锁则有存储的地方)

因此凡是做过类似hashmap.put(k,v)操作且没覆写hashcode的k对象, 以后加锁时,都会直接略过偏向锁。

epoch是什么?

这个属性很多人叫它“偏向时间戳”, 却鲜有人进行详细解释。

主要是因为它涉及到了偏向锁中非常重要的2个优化(批量重偏向和批量撤销)

对于这个epoch,放到下文的偏向锁解锁过程进行解释。

你可以先简单理解为,通过epoch,jvm可以知道这个对象的偏向锁是否过期了,过期的情况下允许直接试图抢占,而不进行撤销偏向锁的操作。

偏向锁运作详解

偏向锁上锁时,如何避免冲突和竞争?

我们知道偏向锁其实就是将线程id设置了进去,但是如果存在冲突怎么办?

因此,jmv会通过CAS来设置偏向线程id,一旦设置成功那么这个偏向锁就算挂上了。

后面每次访问时,检查线程id一致,就直接进入同步代码块执行了。

CAS概念补充:

CAS是一个原子性操作, 调用者需要给定修改变量的期望值 和 最终值

当内存中该变量的值和期望值相等时,才更新为最终值, 这个相等的比较和更新的操作是原子操作

对于到偏向锁加锁过程, 其实就是先取出线程id部分, 如果为空, 则进行(期望值:空 , 最终值:当前线程id)的CAS操作, 如果发现期望值不匹配,就说明被抢先了 。

离开同步代码块时, markword中的线程id会重新变为0吗?

并不会,这个偏向锁线程id会一直挂着, 后面只要识别到id一致,就不用做特殊处理。

偏向锁发生竞争时的切锁或者升级操作。

但当有其他线程来访问时,之前设置的偏向锁就有问题了,说明存在多线程访问同一个对象的情况。

注意!!!这里并非像很多资料里说的那样, 一旦发生多线程调用, 偏向锁就升级成轻量级锁,而是做了很多的细节处理,来尽可能避免轻量级锁这种耗费CPU的操作。

首先,jvm考虑到了这种场景:

最开始1h内,都是线程A在调用大量的对象obj, 于是偏向锁一直都是线程A。

后来线程A不跑了, 对象obj的调用交给了线程B,即未来都是线程B来调用。

那么这时候,有必要马上升级轻量级锁吗?

没必要!因为未来仍然是单线程调用,仅仅是线程不同而已,也许可以尝试仍旧用偏向锁?

于是就有了如下的撤销偏向锁的动作:

  1. 当线程B发现是偏向锁,且线程id不为自己时,开始撤销操作
  2. 首先,线程B会一直等待 对象obj 到达jvm安全点
  3. 到达安全点后, 线程B检查线程A是否正处在obj的同步代码块内。
  4. 如果线程A正在同步代码块中, 则没得商量了,直接升级为轻量级锁。
  5. 如果线程A不在同步代码块中, 那么线程B还有机会, 它先把偏向锁改成无锁状态,然后再用CAS的方式尝试重新竞争,如果能竞争到,那么就会偏向自己。

完整过程如下图所示:

image.png

为什么要等待安全点,才能做撤销操作?

这是为了保证撤销操作的安全性。否则可能出现jvm正在撤销的时候, 另一个线程又开始对该对象做操作,引发错误。

为什么要先退化成无锁状态,再试图竞争成偏向锁?不能直接偏向吗?

因为你无法预测A是否会卷土重来,置成无锁后, A和B可以公平竞争。

为什么原偏向线程在同步代码块中时,就必须升级为轻量级锁?能否同样撤销无锁来竞争?

不可以,因为同步代码块还在执行的话,那B线程此时是注定无法立刻得到锁的,注定了它必须升级为轻量级锁,通过轻量级锁中的循环能力来做获取锁的操作。

批量重偏向,以及epoch的应用

上文提到, 线程B重新抢偏向锁时,会试图等待安全点,撤销成无锁,再做公平抢占。 这个动作还是比较费时的。

假设有一个场景, 我们new 了30个obj对象, 最初都是由A线程使用,后面通过for循环都由B线程使用,那么会发现在很短的时间内,连续发生了偏向锁撤销为无锁,且未因同步块竞争而发生轻量升级的情况。

那么,jvm猜测此时后面都是类似的情况,于是B线程调用obj对象时,不再撤销了,直接CAS竞争threadId,因为jvm预测A不会来抢了,具体步骤如下所示:

  1. jvm会在obj对象的类class对象中, 定义了一个偏向撤销计数器以及epoch偏向版本。

  2. 每当有一个对象被撤销偏向锁, 都会让偏向撤销计数器+1。

  3. 一旦加到20, 则认为出现大规模的锁撤销动作。 于是class类对象中的epoch值+1(但是epoch一般只有2位即0~3)。

  4. 接着, jvm会找到所有正处在同步代码块中的obj对象, 让他的epoch等于class类对象的epoch。

  5. 其他不在同步代码块中的obj对象,则不修改epoch。

  6. 当B线程来访问时,发现obj对象的epoch和class对象的epoch不相等,则不再做撤销动作,直接CAS抢占。 因为当epoch不等时,这说明该obj对象之前一直没被原主人使用, 但它的兄弟们之前纷纷投降倒戈了, 那我应该直接尝试占用就好,没必要那么谨慎了!

详细过程如下图所示:

image.png

批量撤销

但如果短时间内该类的撤销动作超过40个, jvm会认为这个数量太多了, 不保险,数量一多,预测就不准了。

jvm此时会将 obj对象的类class对象中的偏向标记**(注意是类中的偏向锁开启标记,而不是对象头中的偏向锁标记)**设置为禁用偏向锁。 后续该对象的new操作将直接走轻量级锁的逻辑。

image.png

偏向锁在进程一开始就启用了吗

即使你开启了偏向锁,但是这个偏向锁的启用是有延迟,大概 4s左右。

即java进程启动的4s内,都会直接跳过偏向锁,有同步代码块时直接使用轻量级锁。

原因是 JVM 初始化的代码有很多地方用到了synchronized,如果直接开启偏向,产生竞争就要有锁升级,会带来额外的性能损耗,jvm团队经过测试和评估, 选择了启动速度最快的方案, 即强制4s内禁用偏向锁,所以就有了这个延迟策略 (当然这个延迟时间也可以通过参数自己调整)

偏向锁的重要演变历史和思考

偏向锁在JDK6引入, 且默认开启偏向锁优化, 可通过JVM参数-XX:-UseBiasedLocking来禁用偏向锁。

jdk的演变过程中, 为偏向锁做了如上所述的批量升级、撤销等诸多动作。

但随着时代发展,发现偏向锁带来的维护、撤销成本, 远大于轻量级锁的少许CAS动作。

官方说明中有这么一段话: since the introduction of biased locking into HotSpot also change the amount of uncontended operations needed for that relation to remain true。

随着硬件发展,原子指令成本变化,导致轻量级自旋锁需要的原子指令次数变少(或者cas操作变少 个人理解),所以自旋锁成本下降,故偏向锁的带来的优势就更小了

于是jdk团队在Jdk15之后, 再次默认关闭了偏向锁

也许你会问,那前面学习了那么一堆还有啥意义,都不推荐使用了。

但大部分java应用还是基于jdk8开发的, 并且偏向锁里的思想还是值得借鉴的。

还有就是奥卡姆剃刀原理, 如果增加的内容带来很大的成本,不如大胆的废除掉,接受一点落差,将精力放在提升度更大的地方。

轻量级锁

轻量级锁的markword如下所示,可以看到除了锁状态标记位,其他的都变成了一个栈帧中lockRecord记的地址。

image.png

原先markword中的信息都去哪里了?

之前提到markword中有分代年龄、cms_free、hashcode等固有属性。

这些信息会被存储到对应线程栈帧中的lockRecord中。

lockRecord格式以及存储/交换过程如下:

image.png

**另外注意, 当轻量级锁未上锁时, 对象头中的markword存储的还是markword内容,并没有变成指针,只有当上锁过程中,才会变成指针。 **

因此轻量级锁是存在反复的加锁解锁操作的(偏向锁只有在更换偏向线程时才会有类似动作)

解锁过程同理,通过CAS,将对象头替换回去。

轻量级锁如何处理线程重入问题?

对于同一个线程,如果反复进入同步块,在sync语义上来说是支持重入的(即持有锁的线程可以多次进入锁区域), 对轻量级锁而言,必须实现这个功能。

因此线程的lockRecord并非单一成员,他其实是一个lockRecord集合,可以存储多个lockRecord

每当线程离开同步块,lockRecord减少1个, 直到这个lockReocrd中包含指针,才会做解锁动作。

image.png

轻量级锁加锁过程

根据上述CAS和重入相关,可以得到进入同步代码块时的加锁过程:

  1. 进入同步块前,检查是否已经储存了lockRecord地址,且地址和自己当前线程一致 。如果已经存了且一致,说明正处于重入操作,走重入逻辑,新增lockRecord

  2. 如果未重入,检查lockRecord是否被其他线程占用,如果被其他线程占用,则自旋等待,自旋超限后升级重量级锁

  3. 如果未重入,且也没被其他线程占用,则取出lockRecord中存的指针地址,然后再用自己的markword做CAS替换

  4. 替换失败,则尝试自旋重新CAS,失败次数达到上限,也一样升级

    image.png

轻量级锁的解锁流程

根据上面重入的问题,可以得到轻量级锁的退出流程如下:

image.png

自旋次数的上限一定是10次吗?

在JDK 6中对自旋锁的优化,引入了自适应的自旋。

自适应意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环。

另一方面,如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行时间的增长及性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准,虚拟机就会变得越来越“聪明”了

重量级锁

重量级锁如下:

每个对象会有一个objectMonitor的C++对象生成, 通过地址指向对方,后面的逻辑都是通过C++来实现。

image.png

升级为重量级锁的条件

  1. 从轻量级锁升级为重量级锁的条件: 自旋超过10次 或者达到自适应自旋上限次数

  2. 从无锁/偏向锁直接升级为重量级锁的条件:调用了object.wait()方法,则会直接升级为重量级锁!

    第二个条件容易被忽略的

markword去哪了

对象头中的markwod,和轻量级锁中的处理类似, 被存入了objectMonitor对象的header字段中了。

重量级锁同步的原理图解

每个对象的重量级锁指向一个独有的objectMonitor

这个对象是C++实现的

里面的东西比较多,内容非常复杂,里面关于cxq、entryList、qmod之间的关系非常复杂,这里只会简单解释部分过程,不一定全部正确或者包含所有细节。

因此特地拿出一句我认为说的很好的话:

“与其费劲心机研究C++实现的objectMonitor,不如去研究使用java实现的AQS原理,二者的核心思想大部分一致,AQS源码在语言上对java人而言更为友好 ,能让你更好理解线程排队、等待、唤醒的各种过程”

但这篇文章毕竟说的是sync关键字,所以还是简要说一下monitor的过程:

  1. 当线程第一次调用monitorEntry执行且是重量级锁的情况下,会先进入cxq队列

    image.png

  2. 当涉及锁的频繁竞争且需要阻塞时,需要进入entryList队列中。

    image.png

  3. 如果线程能CAS竞争到onwer指针,就说明占有同步代码块成功, 如果CAS竞争不到,则block阻塞。

    image.png

  4. monitorExit退出时,会让entryList中block阻塞的线程唤醒重新竞争

    image.png

  5. 如果调用了object.wait()方法, onwer线程会进入等待队列(注意,因为竞争失败的线程,不会进入waitSet,waitSet只服务于那些wait()方法引发的线程)

    image.png

  6. 当调用的object.notify()或者notifyAll, 等待队列中的线程会根据qmod模式的不同,进入cxq或者进入entryList。

    image.png

简要版流程如下:

image.png

关于synchronized关键字的思考

终于写完了,说点其他的。

众所周知,随着jdk的不断升级, 官方提供的JUC以及衍生同步组件越来越强大, sync与其相比,功能相当少,背后逻辑却异常复杂,甚至因为过于复杂,还在中间对偏向锁的功能进行了默认关闭的操作。

那么这个关键字是否还有存在的必要呢?

首先,很多历史代码以及内部某些jdk代码实现,都还是会依赖这个关键字进行同步处理,没法全部替换成AQS。

另外,不考虑背后升级的复杂逻辑, sync使用起来绝对是比JUC简单很多的, 当你的场景很简单,但确实有同步的问题, 用sync会提升不少开发的效率。

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

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

相关文章

后端之路——文件本地上传

一、基础原理 文件上传是一个很基础的知识点&#xff0c;尤其是本地上传&#xff0c;在现实开发基本都是云上传&#xff0c;但是作为一个基础要简单了解一下 首先前端我就不多讲解了&#xff0c;网页开发里用<form>表单可以上传文件&#xff0c;只需要加上这三属性&…

防火墙基础实验配置

一&#xff0c;实验拓扑 二&#xff0c;实验需求&#xff1a; 1.DMZ区内的服务器&#xff0c;办公区仅能在办公时间内&#xff08;9&#xff1a;00 - 18&#xff1a;00&#xff09;可以访问&#xff0c;生产区的设备全天可以访问 2.生产区不允许访问互联网&#xff0c;办公区…

如何批量更改很多个文件夹里的文件名中包含文件夹名?

&#x1f3c6;本文收录于《CSDN问答解惑-专业版》专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收…

共生与变革:AI在开发者世界的角色深度剖析

在科技日新月异的今天&#xff0c;人工智能&#xff08;AI&#xff09;已不再是遥不可及的概念&#xff0c;而是逐步渗透到我们工作与生活的每一个角落。对于开发者这一群体而言&#xff0c;AI的崛起既带来了前所未有的机遇&#xff0c;也引发了关于其角色定位的深刻讨论——AI…

STM32-Unix时间戳和BKP备份寄存器以及RTC实时时钟

本内容基于江协科技STM32视频学习之后整理而得。 文章目录 1. Unix时间戳1.1 Unix时间戳简介1.2 UTC/GMT1.3 时间戳转换 2. BKP备份寄存器2.1 BKP简介2.2 BKP基本结构2.3 BKP库函数 3. RTC实时时钟3.1 RTC简介3.2 RTC框图3.3 RTC基本结构3.4 硬件电路3.5 RTC操作注意事项3.6 R…

修改服务器挂载目录

由于我们的项目通常需要挂载一个大容量的数据盘来存储文件数据&#xff0c;所以我们每台服务器都需要一个默认的挂载目录来存放这些数据&#xff0c;但是由于我们的误操作&#xff0c;导致挂载目录名字建错了&#xff0c;这时候后端就读不到挂载目录了&#xff0c;那我们我们的…

在mysql中delete和truncated的相同点和区别点

相同点 删除数据&#xff1a;两者都会删除表中的数据。影响数据&#xff1a;两者都不删除表结构&#xff0c;只影响表中的数据。 区别点 操作方式&#xff1a; DELETE&#xff1a;逐行删除数据&#xff0c;可以使用 WHERE 子句来指定删除的条件。如果不加 WHERE 子句&#…

NI 5G大规模MIMO测试台:将理论变为现实

目录 概览引言MIMO原型验证系统MIMO原型验证系统硬件LabVIEW通信系统设计套件&#xff08;简称LabVIEW Communications&#xff09;CPU开发代码FPGA代码开发硬件和软件紧密集成 LabVIEW Communications MIMO应用框架MIMO应用框架特性单用户MIMO和多用户MIMO基站和移动站天线数量…

Hive 高可用分布式部署详细步骤

目录 系统版本说明 hive安装包下载及解压 上传mysql-connector-java的jar包 配置环境变量 进入conf配置文件中&#xff0c;将文件重命名 在hadoop集群上创建文件夹 创建本地目录 修改hive-site.xml文件 同步到其他的节点服务器 修改node02中的配置 hive-site.xml 修改…

如何在多个服务器上安装WordPress分布式部署

许多网络主机现在保证其服务的正常运行时间为 99.9%&#xff0c;但这仍然每年最多有 8.7 小时的停机时间。 许多公司不能够承担这种风险。例如。在超级碗比赛中失败的体育新闻网站可能会失去忠实的追随者。 我们通过设置维护高可用性 WordPress分布式部署配置来帮助 WordPres…

C基础day8

一、思维导图 二、课后习题 #include<myhead.h> #define Max_Stu 100 //函数声明 //学生信息录入函数 void Enter_stu(int *Num_Stu,char Stu_name[][50],int Stu_score[]); //查看学生信息 void Print_stu(int Num_Stu,char Stu_name[][50],int Stu_score[]); //求出成绩…

latex英文转中文word,及一些latex相关工具分享

前言&#xff1a;想要转换latex生成的英文pdf文件为中文word文件 一、主要步骤 1、文字翻译&#xff1a;直接使用谷歌翻译等辅助将英文翻译成中文即可&#xff1b; 支持英文pdf文件全文翻译&#xff0c;再用迅捷PDF转换器之类的转成word&#xff0c;再手动调整。 https://app…

网络编程:各协议头(数据报格式)

一、mac头 二、ip头 protocol——tcp/udp &#xff08;7&#xff09;TTL——生存时间 三、tcp头 四、udp头

第三课网关作用

实验拓扑图&#xff1a; 基础配置&#xff1a; PC1的基础配置 PC2的基础配置&#xff1a; PC4的基础配置 AR1添加PC4网段: 并且添加pc1,pc2的网段。 并且添加pc1,pc2的网段。 原理&#xff1a;PC4先把数据交给100.100.100.1&#xff0c;交给了路由器&#xff0c;路由器再把数…

ARM学习(29)NXP 双coreMCU IMX1160学习----NorFlash 启动引脚选择

ARM学习&#xff08;28&#xff09;NXP 双coreMCU IMX1160学习----NorFlash 启动引脚选择 1、多种启动方式介绍 IMX1166 支持多组flexSPI 引脚启动&#xff0c;FlexSPI1以及FlexSPI2&#xff0c;通过boot cfg可以切换FlexSPI得实例。 每个实例又支持多组引脚&#xff0c;总共…

《Nature》文章:ChatGPT帮助我学术写作的三种方式

图片翻译 ** 文章内容** 忏悔时间&#xff1a;我使用生成式人工智能&#xff08;AI&#xff09;。尽管在学术界关于聊天机器人是积极力量还是消极力量的争论不休&#xff0c;但我几乎每天都使用这些工具来完善我所写论文中的措辞&#xff0c;并寻求对我被要求评估的工作进行替…

Postman使用教程【项目实战】

目录 引言软件下载及安装项目开发流程1. 创建项目2. 创建集合(理解为&#xff1a;功能模块)3. 设置环境变量&#xff0c;4. 创建请求5. 测试脚本6. 响应分析7. 共享与协作 结语 引言 Postman 是一款功能强大的 API 开发工具&#xff0c;它可以帮助开发者测试、开发和调试 API。…

.NET周刊【7月第1期 2024-07-07】

国内文章 学习.NET 8 MiniApis入门 https://www.cnblogs.com/hejiale010426/p/18280441 MiniApis是ASP.NET Core中的轻量级框架&#xff0c;用最少的代码和配置创建HTTP API。其特点包括简洁明了、性能卓越、灵活多变、易于学习使用&#xff0c;并与ASP.NET Core生态系统完美…

数学建模中常用的数据处理方法

常用的数据处理方法 本文参考 B站西电数模协会的讲解视频 &#xff0c;只作笔记提纲&#xff0c;想要详细学习具体内容请观看 up 的学习视频。国赛的 C 题一般数据量比较大。 这里介绍以下两种方法&#xff1a; 数据预处理方法 数据分析方法 数据预处理方法 1. 数据清洗 为…

伦敦银看盘一般看什么 这3样东西不能缺少

伦敦银看盘&#xff0c;是指伦敦银市场开市之后&#xff0c;投资者打开走势图表&#xff0c;观察盘面行情和盘口信息的过程。一般来说&#xff0c;懂得看盘的人可能会被贴上专业的标签&#xff0c;我们在各种影视作品中看到&#xff0c;那些华尔街的交易员坐在电脑面前&#xf…