java原子操作类实现原理
文章目录
- AtomicLong实现原理
- 递增和递减操作代码
- 总结
- LongAdder实现原理
- 实现原理
- LongAdder 代码分析
- 构造方法
- sum方法
- reset方法
- sumThenReset方法
- longValue方法
- add 方法
- longAccumulate 方法
- 总结
JUC 包提供 了一系列的原子性操作类,这些类都是使用非阻塞算法 CAS 实现的 ,相比使用锁实现原子性操作这在性能上有很大提高。本篇主要以 AtomicLong 为例进行原子操作类的讲解,找到原子操作类的不足并提出解决方案。
AtomicLong实现原理
AtomicLong 是原子性递增或者递减类,其内部使用 Unsafe 来实现,由于AtomicLong 也是在rt包下,都是由BootStarp 类加载器加载,所以获取Unsafe 时直接使用 Unsafe.getUnsafe ( )方法就可以获取到。
递增和递减操作代码
// ( 6 )调用 unsafe方法, 原子性设置value值为原始值+1 ,返回值为递增后的值
public final long incrementAndGet() {
return unsafe.getAddAddLong(this, valueOffset , lL) + 1L ;
}
// ( 7 )调用 unsafe方法,原子性设置 value{直为原始值- 1 ,返回值为递减之后的值
public final long decrementAndGet() {
return unsafe . getAddAddLong(this, valueOffset , - lL) - 1L ;
}
// (8 )调用 unsafe方法,原子性设置value值为原始值+1 , 返回值为原始值
public final long getAndincrement() {
return unsafe .getAndAddLong(this , valueOffset , 1L) ;
}
// ( 9 )调用 unsafe方法,原子性设置 value值为原始值- 1 ,返回位为原始值
public final long getAndDecrement( ) {
return unsafe.getAndAddLong (this , valueOffset ,- 1L) ;
}
在如上代码内部都是通过调用 Unsafe 的 getAndAddLong 方法来实现操作,这个函数
是个原子性操作,这里第一个参数是 AtomicLong 实例的引用 , 第二个参数是 value 变量在 AtomicLong 中的偏移值,第三个参数是要设置的第二个变量的值。
其中, getAndlncrement 方法在 JDK 7 中的实现逻辑为
public final long getAndincrement () {
while (true) {
long current= get() ;
long next = current + 1 ;
if (compareAndSet(current , next))
return current ;
}
}
在如上代码中,每个线程是先拿到变量的当前值(由于 value 是 volatile 变量,所以这
里拿到的是最新的值),然后在工作内存中对其进行增 1 操作 ,而后使用 CAS 修改变量的值。如果设置失败,则循环继续尝试 , 直到设置成功 。
而 JDK8 中的逻辑为
public final long getAndAddLong(Object paramObject , long paramLongl , long paramLong2)
{
long l ;
do {
l = getLongvolatile(paramObject , paramLongl) ;
) while (!compareAndSwapLong(param0bject , paramLong1 , 1, 1 + paramLong2) );
return l ;
}
可以看到, JDK 7 的 AtomicLong 中的循环逻辑已经被 JDK 8 中的原子操作类 UNsafe
内置了 , 之所以内置应该是考虑到这个函数在其他地方也会用到,而内 置可以提高复用性 。
public final boolean compareAndSet(long expect , long update) {
return unsafe.compareAddSwapLong ( this , valueOffset , expect , update) ;
}
由如上代码可知,在内部还是调用了 unsafe.compareAndSwapLong 方法 。 如果原子变量中的 value 值等于expect则使用 update 值更新该值并返回 true,否则返回 false 。
总结
经过上面代码的介绍,我们发现cas的实现是依赖于 unsafe 实现的,在进行递增递减时会采用循环的方式去cas更新原始值。这样的好处是不需要使用锁进行阻塞来保障数据安全,直接利用硬件自带的比较替换方式更新数据,相对来说更加轻量级。但是如果在高并发下,这种比较替换会十分频繁,导致CPU不断被占用,进而导致监控中出现CPU使用告警。为解决这个问题,在JDK8中出现了LongAdder。
LongAdder实现原理
既然 AtomicLong 的性能瓶颈是由于过 多 线程同时去竞争一个变量的更新而产生的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源 ,是不是就解决了性能问题?是的, LongAdder 就是这个思路 。
实现原理
使用 LongAdder 时,则是在内部维护多个 Ce ll 变量,每个 Cell 里面有一个初始值为 0 的 long 型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相地减少了 争夺共享资源的并发量。另 外,多个线程在争夺同一个 Cell 原子变量时如果失败了 , 它并不是在当前 Cell 变量上一直自旋 CAS 重试,而是尝试在其他 Cell 的变量上进行 CAS 尝试 ,这个改变增加了当前线程重试 CAS 成功的可能性 。最后,在获取 LongAdder 当前值时, 是把所有 Cell 变量的 value 值累加后再加上 base返回的 。
LongAdder 维护了 一个延迟初始化的原子性更新数组(默认情况 下 Cell 数组是 null )和 一个基值变量 base 。 由于 Cells 占用的内存是相对比较大的,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。
当一开始判断 Cell 数组是 null 并且并发线程较少时,所有 的 累加操作都是对 base 变量进行的 。 保持 Ce ll 数组的大小为 2 的 N 次方,在初始化时 Cell 数组中的 Cell 元素个数为 2,数组里面的变量实体是 Cell 类型。 Cell 类型是 AtomicLong 的一个改进,用来减少缓存的争用,也就是解决伪共享问题 。
对于大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无规律地分散在内存中的 (也就是说多个原子性变量的内存地址是不连续 的), 多个原子变量被放入同 一个缓存行的可能性很小 。 但是原子性数组元素的内存地址是连续的,所以数组内的 多个元素能经常共享缓存行,因此这里使用 @sun.misc.Contended 注解对Cell 类进行字节填充,这防止了 数组中多个元素共享一个缓存行,在性能上是一个提升。
LongAdder 代码分析
下面围绕以下话题从源码角度来分析 LongAdder 的实现:
LongAdder 类继承自 Striped64 类,在 Striped64 内部维护着三个变量。LongAdder 的真实值其实是 base 的值与 Cell 数组里面所有 Cell 元素中的 value 值的累加,base 是个基础值,默认为 0 。 cellsBusy 用来实现自旋锁,状态值只有 0 和 l ,当创建 Cell 元素,扩容 Cell 数组或者初始化 Cell 数组时,使用 CAS 操作该变量来保证同时只有一个线程可以进行其中之一的操作 。
构造方法
``
@sun .misc.Contended static final class Cell {
volatile long value ;
Cell (long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this , valueOffset , cmp , val) ;
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE ;
private static final long valueOffset ;
static {
try {
UNSAFE = sun. misc .Unsafe . getUnsafe() ;
Class<?> ak =Cell . class ;
valueOffset = UNSAFE . objectFieldOffset
(ak . getDeclaredField (” value” )) ;
} catch (Exception e) {
throw new Error(e) ;
}
}
}
``
可以看到, Cell 的构造很简单,其内部维护一个被声明为 volatile 的变量 , 这里声 明
为 volatile 是因为线程操作 value 变量时没有使用锁 , 为 了 保证变量的内存可见性这里将其声 明为 volatile 的 。另外 cas 函数通过 CAS 操作,保证了当前线程更新时被分配的 Cell 元素 中 value 值的原子性。另外 , Cell 类使用@sun.misc .Contended 修饰是为了避免伪共享。
sum方法
long sum()返回 当前的值 ,内 部操作是累加所有 Cell 内 部的 value 值后再累加 base 。
例如下面的代码 , 由于计算总和时没有对 Cell 数组进行加锁,所以在累加过程中
可能有其他线程对 Cell 中 的值进行了修改 , 也有可能对数组进行了扩容,所 以 sum
返回的值并不是非常精确的, 其返回值并不是一个调用 sum 方法时的原子快照值 。
``
public long sum() {
Cell[] as = cells; Cell a;long sum = base ;if (as != null) {for (int i = 0 ; i < as.length ; ++i) {if ( (a = as[i] ) != null)sum += a . value;} }
return sum;
}
``
reset方法
void reset()为重置操作 , 如下代码把 base 置为 0 , 如果 Cell 数组有元素 ,则元素值被重置为 0 。
``
public void reset () {
Cell[] as= cells ; Cell a;
base = 0L ;
if (as 1= null ) {
for (int i = 0 ; i< as . length ; ++i) {
if ((a=as[i]) != null) {
a.value = 0L ;
}
}
}
``
sumThenReset方法
long sumThenReset() 是 sum 的改造版本,在使用 sum 累加对应的 Cell 值后,
把当前 Cell 的值重置为 0, base 重置为 0。这样 , 当多线程调用该方法时会有问题,
比如考虑第一个调用 线程清空 Cell 的值,则后一个线程调用时累加的都是 0 值 。
longValue方法
long longValue() 等价于 sum() 。
add 方法
``
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);}
}
final boolean casBase(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
``
代码 (1)首先看 cells 是否为 null ,如果为 null 则当前在基础变量 base 上进行累加 ,
这时候就类似 AtomicLong 的操作 。
如果 cells 不为 null 或者线程执行代码( 1 )的 CAS 操作失败了, 则会去执行代码 。 ) 。代码 ( 2 )( 3 )决定当前线程应该访 问 cells 数组里面的哪一个 Cell 元素,如果当前线程映射的元素存在则执行代码(4),使用 CAS 操作去更新分配的 Ce ll 元素 的 value 值,如果当前线程映射的元素不存在或者存在但是 CAS 操作失败则执行代码( 5 )。其实将代码(2)(3) (4 )合起来看就是获取当前线程应该访问的 cells 数组的 Cell 元素,然后进行 CAS 更新操作,只是在获取期间如果有些条件不满足则会跳转到代码( 5 )执行。另外当前线程应该访 问 cells 数组的哪一个 Cell 元素是通过 getProbe() & m 进行计算的 , 其中 m 是当前cells 数组元素个数 - 1 , getProbe()则用于获取 当前线程中变量 threadLocalRandomProbe 的值,这个值一开始为 0,在代码( 5 )里面会对其进行初始化。并且当前线程通过分配的Cell 元素的 cas 函数来保证对 Cell 元素 value 值更新的原子性。
longAccumulate 方法
这是 cells 数组被初始化和扩容的地方。
``
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
int h;if ((h = getProbe()) == 0) {ThreadLocalRandom.current(); // force initializationh = getProbe();wasUncontended = true;}boolean collide = false; // True if last slot nonemptyfor (;;) {Cell[] as; Cell a; int n; long v;if ((as = cells) != null && (n = as.length) > 0) {if ((a = as[(n - 1) & h]) == null) {if (cellsBusy == 0) { // Try to attach new CellCell r = new Cell(x); // Optimistically createif (cellsBusy == 0 && casCellsBusy()) {boolean created = false;try { // Recheck under lockCell[] rs; int m, j;if ((rs = cells) != null &&(m = rs.length) > 0 &&rs[j = (m - 1) & h] == null) {rs[j] = r;created = true;}} finally {cellsBusy = 0;}if (created)break;continue; // Slot is now non-empty}}collide = false;}else if (!wasUncontended) // CAS already known to failwasUncontended = true; // Continue after rehashelse if (a.cas(v = a.value, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break;else if (n >= NCPU || cells != as)collide = false; // At max size or staleelse if (!collide)collide = true;else if (cellsBusy == 0 && casCellsBusy()) {try {if (cells == as) { // Expand table unless staleCell[] rs = new Cell[n << 1];for (int i = 0; i < n; ++i)rs[i] = as[i];cells = rs;}} finally {cellsBusy = 0;}collide = false;continue; // Retry with expanded table}h = advanceProbe(h);}else if (cellsBusy == 0 && cells == as && casCellsBusy()) {boolean init = false;try { // Initialize tableif (cells == as) {Cell[] rs = new Cell[2];rs[h & 1] = new Cell(x);cells = rs;init = true;}} finally {cellsBusy = 0;}if (init)break;}else if (casBase(v = base, ((fn == null) ? v + x :fn.applyAsLong(v, x))))break; // Fall back on using base}
}
``
当每 个线程第 一次 执行到代码 ( 6 )时,会初始化当前线程变 量threadLocalRandomProbe 的值,上面也说了,这个变量在计算当前线程应该被分配到 cells数组的哪一个 Cell 元素时会用到 。
cells 数组的初始化是在代码(14)的中进行的 , 其中 cellsBusy 是一 个标示 , 为 0 说明当前 cells 数组没有在被初始化或者扩容, 也没有在新建 Cell 元素,为 1则说明 cells 数组在被初始化或者扩容,或者当前在创建新的 Cell 元素、通过 CAS 操作来进行 0 或 1状态的切换,这里使用 casCellsBusy 函数。假设当 前线程通过 CAS 设置 cellsBusy 为 1,则当前线程开始初始化操作,那么这时候其他线程就不能进行扩容了 。 如代码( 14.1 )初始化cells 数组元 素个数为 2 ,然后使用 h&1 计 算当前线程应该访问 celll 数组的哪个位置,也就是使用当前线程的 threadLocalRandomProbe 变量值& ( cells 数组元素个数- 1 ),然后标示 cells 数组已经被初始化,最后代码( 14.3 ) 重置了 cellsBusy 标记 。 显然这里没有使用CAS 操作,却是线程安全的,原因是cellsBusy 是 volatile 类型的,这保证了变量的内存可见性,另外此时其他地方的代码没有机会修改 cellsBusy 的值 。 在这里初始化的 cells 数组里面的两个元素的值目前还是 null 。
cells 数组的扩容是在代码 (12 )中进行的,对 cells 扩容是有条件的,也就是代码( 10) ( 11 )的条件都不满足的时候 。具体就是当前 cells 的元素个数小于当前机器 CPU 个数并且当前多个线程访 问了 cells 中同 一个元素从而导致冲突使其中 一个线程 CAS 失败时才会进行扩容操作。这里为何要涉及 CPU 个数呢?其实在基础篇中己经讲过 , 只有当每个 CPU 都运行一个线程时才会使多线程的效果最佳,也就是当 cells 数组元素个数与 CPU 个数一致时,每个 Cell 都使用 一个 CPU 进行处理,这时性能才是最佳的 。 代码 (12 )中的扩容操作也是先通过 CAS 设置 cellsBusy 为 1 ,然后才能进行扩容 。 假设 CAS 成功则执行代码(l2.1)将容量扩充为之前的 2 倍,并复制 Cell 元素到扩容后数组 。 另外,扩容后 cells 数组里面除了包含复制过来的元素外,还包含其他新元素,这些元素的值目前还是 null 。
在代码 (7) (8)中,当前线程调用 add 方法并根据当前线程的随机数threadLoca!RandomProbe 和 cells 元 素个数计算要访问的 Cell 元素下标,然后如果发现对应下标元素的值为 null,则新增一个 Cell 元素到 cells 数组,并且在将其添加到 cells 数组之前要竞争设置 cellsBusy 为 1 。
代码( 13 )对 CAS 失败的线程重新计算当前线程的随机值 threadLocalRandomProbe,
以减少下次访问 cells 元素时的冲突机会。
总结
本节介绍了 JDK 8 中新增的 LongAdder 原子性操作类,该类通过内部 cells 数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可 以 同时对 cells数组里面的元素进行并行的更新操作 。 另外,数组元素 Cell 使用@sun .misc .Contended 注解进行修饰 , 这避免了 cells 数组 内 多个原子变量被放入 同 一个缓存行 ,也就是避免了 伪共享,这对性能也是一个提升 。
相关文章:

java原子操作类实现原理
文章目录 AtomicLong实现原理递增和递减操作代码总结 LongAdder实现原理实现原理LongAdder 代码分析构造方法sum方法reset方法sumThenReset方法longValue方法add 方法longAccumulate 方法 总结 JUC 包提供 了一系列的原子性操作类,这些类都是使用非阻塞算法 CAS 实现…...
网络安全-攻击流程-传输层
传输层攻击主要针对OSI模型的第四层,涉及TCP和UDP协议的安全漏洞。以下是常见攻击类型及其流程,以及防御措施: 1. SYN洪水攻击(TCP半连接攻击) 攻击流程: 目标选择:确定目标服务器的IP地址和开…...

【R语言】回归分析
一、线性回归分析 1、lm()函数 lm()函数是用于拟合线性模型(Linear Models)的主要函数。线性模型是一种统计方法,用于描述一个或多个自变量(预测变量、解释变量)与因变量(响应变量)之间的关系…...
在分布式场景下可以使用synchronized加锁么?
首先说结论,在分布式系统中,单纯使用 Java 中的 synchronized 关键字是无法满足需求的,下面从 synchronized 的作用原理、在分布式场景下的局限性以及替代方案等方面详细分析。 一、synchronized 的作用原理 在 Java 中,synchron…...
LeetCodehot 力扣热题100 从前序与中序遍历序列构造二叉树
初始版本 这段代码实现了根据前序遍历和中序遍历重建二叉树。下面我将详细解释每一行的作用,并逐步讲解算法的思路和运行步骤。 代码及注释 class Solution {public:// buildTree 函数用来根据前序遍历(pre)和中序遍历(in)重建二叉树TreeNode* buildTree(vector<…...

Day45(补)【软考】2022年下半年软考软件设计师综合知识真题-计算机软件知识1
文章目录 2022年下半年软考软件设计师综合知识真题第1章 计算机系统基础知识(12/38)计算机软件知识1-6/6哲学概念及收敛思维:是Python程序语言中,处理异常的结构集合,和这个集合之外的结构的区分,考Python集合之外的结构 哲学概念…...
luoguP8764 [蓝桥杯 2021 国 BC] 二进制问题
luogu题目传送门 题目描述 小蓝最近在学习二进制。他想知道 1 到 N 中有多少个数满足其二进制表示中恰好有 K 个 1。你能帮助他吗? 输入格式 输入一行包含两个整数 N 和 K。 输出格式 输出一个整数表示答案。 输入输出样例 输入 #1 7 2 输出 #1 3 说明/提示 对于…...
图形渲染(一)——Skia、OpenGL、Mesa 和 Vulkan简介
1.Skia —— 2D 图形库 Skia 是一个 2D 图形库,它的作用是为开发者提供一个高层次的绘图接口,方便他们进行 2D 图形渲染(比如绘制文本、形状、图像等)。Skia 本身不直接管理 GPU 或进行底层的渲染工作,而是通过 底层图…...

浏览器打开Axure RP模型
1,直接使用chrome打开,提示下载插件 2,需要做一些操作 打开原型文件,找到resources\chrome\axure-chrome-extension.crx文件,这就是我们需要的Chrome插件。 将axure-chrome-extension.crx文件后缀名改为axure-chrome…...
【计算机网络】数据链路层数据帧(Frame)格式
在计算机网络中,数据帧(Frame) 是数据链路层的协议数据单元(PDU),用于在物理介质上传输数据。数据帧的格式取决于具体的链路层协议(如以太网、PPP、HDLC 等)。以下是常见数据帧格式的…...

平面与平面相交算法杂谈
1.前言 空间平面方程: 空间两平面如果不平行,那么一定相交于一条空间直线, 空间平面求交有多种方法,本文进行相关讨论。 2.讨论 可以联立方程组求解,共有3个变量,2个方程,而所求直线有1个变量…...

web集群(LVS-DR)
LVS是Linux Virtual Server的简称,也就是Linux虚拟服务器, 是一个由章文嵩博士发起的自由软件项 目,它的官方站点是 www.linuxvirtualserver.org。现在LVS已经是 Linux标准内核的一部分,在 Linux2.4内核以前,使用LVS时必须要重新编…...

更高效实用 vscode 的常用设置
VSCode 可以说是文本编辑神器, 不止程序员使用, 普通人用其作为文本编辑工具, 更是效率翻倍. 这里分享博主对于 VSCode 的好用设置, 让 VSCode 如虎添翼 进入设置 首先进入设置界面, 后续都在这里进行配置修改 具体设置 每项配置通过搜索关键字, 来快速定位配置项 自动保存…...

win11 终端乱码导致IDE 各种输出也乱码
因为 win11 终端乱码导致IDE 各种输出也乱码导致作者对此十分头大。所以研究了各种方法。 单独设置终端编码对 HKEY_CURRENT_USER\Console 注册表进行修改对 HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processo 注册表进行修改使用命令[Console]::OutputEncoding [Syst…...
对于简单的HTML、CSS、JavaScript前端,我们可以通过几种方式连接后端
1. 使用Fetch API发送HTTP请求(最简单的方式): //home.html // 示例:提交表单数据到后端 const submitForm async (formData) > {try {const response await fetch(http://your-backend-url/api/submit, {method: POST,head…...

Flutter中 List列表中移除特定元素
在 Dart 语言里,若要从子列表中移除特定元素,可以使用以下几种方法,下面为你详细介绍: 方法一:使用 where 方法创建新列表 where 方法会根据指定的条件筛选元素,然后通过 toList 方法将筛选结果转换为新列…...

DeepSeek从入门到精通(清华大学)
DeepSeek是一款融合自然语言处理与深度学习技术的全能型AI助手,具备知识问答、数据分析、编程辅助、创意生成等多项核心能力。作为多模态智能系统,它不仅支持文本交互,还可处理文件、图像、代码等多种格式输入,其知识库更新至2…...
动态规划:解决复杂问题的高效策略
动态规划(Dynamic Programming,简称 DP)是一种在数学、管理科学、经济学、计算机科学等领域中广泛使用的算法设计技术。它通过将复杂问题分解为更简单的子问题,并通过存储子问题的解来避免重复计算,从而高效地解决问题…...
【kafka系列】Kafka事务的实现原理
目录 1. 事务核心组件 1.1 幂等性生产者(Idempotent Producer) 1.2 事务协调器(TransactionCoordinator) 1.3 事务日志(Transaction Log) 2. 事务执行流程 2.1 事务初始化 2.2 发送消息 2.3 事务提…...
网络将内网服务转换到公网上
当然,以下是根据您提供的描述,对内网端口在公网上转换过程的详细步骤,并附上具体例子进行说明: 内网端口在公网上的转换过程详细步骤 1. 内网服务配置 步骤说明: 在内网中的某台计算机(我们称之为“内网…...

【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...

MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...

屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...
三体问题详解
从物理学角度,三体问题之所以不稳定,是因为三个天体在万有引力作用下相互作用,形成一个非线性耦合系统。我们可以从牛顿经典力学出发,列出具体的运动方程,并说明为何这个系统本质上是混沌的,无法得到一般解…...

涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...