阿里云「飞天发布时刻」2024来啦!新产品、新特性、新能力、新方案,等你来探~ 了解详情
写点什么

内存屏障与 JVM 并发

  • 2010-04-21
  • 本文字数:9057 字

    阅读完需:约 30 分钟

内存屏障,又称内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。本文介绍了内存屏障对多线程程序的影响。我们将研究内存屏障与 JVM 并发机制 的关系,如易变量(volatile)、同步(synchronized)和原子条件式(atomic conditional)。本文假定读者已经充分掌握了相关概念和 Java 内存模型,不讨论并发互斥、并行机制和原子性。内存屏障用来实现并发编程中称为 可见性(visibility)的同样重要的作用。

感谢 Brian Goetz 和 Eric Yew 审校本文,同时感谢 Christian Thalinger 提供访问 SPARC 硬件的支持。

内存屏障为何重要?

对主存的一次访问一般花费硬件的数百次时钟周期。处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存操 作的顺序。也就是说,程序的读写操作不一定会按照它要求处理器的顺序执行。当数据是不可变的,同时 / 或者数据限制在线程范围内,这些优化是无害的。如果把 这些优化与对称多处理(symmetric multi-processing)和共享可变状态(shared mutable state)结合,那么就是一场噩梦。当基于共享可变状态的内存操作被重新排序时,程序可能行为不定。一个线程写入的数据可能被其他线程可见,原因是数据 写入的顺序不一致。适当的放置内存屏障通过强制处理器顺序执行待定的内存操作来避免这个问题。

内存屏障的协调作用

内存屏障不直接由 JVM 暴露,相反它们被 JVM 插入到指令序列中以维持语言层并发原语的语义。我们研究几个简单 Java 程序的源代码和汇编指令。首先快速 看一下 Dekker 算法中的内存屏障。该算法利用 volatile 变量协调两个线程之间的共享资源访问。

请不要关注该算法的出色细节。哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域。如果线程在第三行意识到冲突(两个线程都要访问),通 过 turn 变量的操作来解决。在任何时刻只有一个线程可以访问关键区域。

复制代码
// code run by first thread // code run by second thread
1 intentFirst = true; intentSecond = true;
2
3 while (intentSecond) while (intentFirst) // volatile read
4 if (turn != 0) { if (turn != 1) { // volatile read
5 intentFirst = false; intentSecond = false;
6 while (turn != 0) {} while (turn != 1) {}
7 intentFirst = true; intentSecond = true;
8 } }
9
10 criticalSection(); criticalSection();
11
12 turn = 1; turn = 0; // volatile write
13 intentFirst = false; intentSecond = false; // volatile write

硬件优化可以在没有内存屏障的情况下打乱这段代码,即使编译器按照程序员的想法顺序列出所有的内存操作。考虑第三、四行的两次顺序 volatile 读操 作。每一个线程检查其他线程是否发信号想进入关键区域,然后检查轮到谁操作了。考虑第 12、13 行的两次顺序写操作。每一个线程把访问权释放给其他线程, 然后撤销自己访问关键区域的意图。读线程应该从不期望在其他线程撤销访问意愿后观察到其他线程对 turn 变量的写操作。这是个灾难。但是如果这些变量没有 volatile 修饰符,这的确会发生!例如,没有 volatile 修饰符,第二个线程在第一个线程对 turn 执行写操作(倒数第二行)之前可能会观察到 第一个线程对 intentFirst(倒数第一行) 的写操作。关键词 volatile 避免了这种情况,因为它在对 turn 变量的写操作和对 intentFirst 变量的写操作之间创建了一个先后关系。编译器无法重新排序这些写操作,如果必要,它会利用一个内存屏障禁止处理器重排序。让我们来 看看一些实现细节。

PrintAssembly HotSpot 选项是 JVM 的一个诊断标志,允许我们获取 JIT 编译器生成的汇编指令。这需要最新的 OpenJDK 版本或者新 HotSpot update14 或者更高版本。通过需要一个反编译插件。Kenai 项目提供了用于 Solaris、Linux 和 BSD 的插件二进制文件。hsdis 是另 一款可以在 Windows 通过源码构建的插件。

两次顺序读操作的第一次(第三行)的汇编指令如下。指令流基于 Itanium 2 多处理硬件、JDK 1.6 update 17。本文的所有指令流都在左手边以行号标记。相关的读操作、写操作和内存屏障指令都以粗体标记。建议读者不要沉迷于每一行指令。

复制代码
1 0x2000000001de819c: adds r37=597,r36;; ;...84112554
<b>2 0x2000000001de81a0: ld1.acq r38=[r37];; ;...0b30014a a010</b>
3 0x2000000001de81a6: nop.m 0x0 ;...00000002 00c0
4 0x2000000001de81ac: sxt1 r38=r38;; ;...00513004
5 0x2000000001de81b0: cmp4.eq p0,p6=0,r38 ;...1100004c 8639
6 0x2000000001de81b6: nop.i 0x0 ;...00000002 0003
7 0x2000000001de81bc: br.cond.dpnt.many 0x2000000001de8220;;

简短的指令流其实内容丰富。第一次 volatile 位于第二行。Java 内存模型确保了 JVM 会在第二次读操作之前将第一次读操作交给处理器,也就是按照 “程序的顺序”——但是这单单一行指令是不够的,因为处理器仍然可以自由乱序执行这些操作。为了支持 Java 内存模型的一致性,JVM 在第一次读操作上添 加了注解 ld.acq,也就是“载入获取”(load acquire)。通过使用 ld.acq,编译器确保第二行的读操作在接下来的读操作之前完成。问题就解决了。

请注意这影响了读操作,而不是写。内存屏障强制读写操作顺序限制不是单向的。强制读写操作顺序限制的内存屏障是双向的, 类似于双向开的栅栏。使用 ld.acq 就是单向内存屏障的例子。

一致性具有两面性。如果一个读线程在两次读操作之间插入了内存屏障而另外一个线程没有在两次写操作之间添加内存屏障又有什么用呢?线程为了协调,必须同时 遵守这个协议,就像网络中的节点或者团队中的成员。如果某个线程破坏了这个约定,那么其他所有线程的努力都白费。Dekker 算法的最后两行代码的汇编指 令应该插入一个内存屏障,两次 volatile 写之间。

$ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes -XX:CompileCommand=print,WriterReader.write WriterReader

复制代码
1 0x2000000001de81c0: adds r37=592,r36;; ;...0b284149 0421
2 0x2000000001de81c6: st4.rel [r37]=r39 ;...00389560 2380
3 0x2000000001de81cc: adds r36=596,r36;; ;...84112544
4<b> 0x2000000001de81d0: st1.rel [r36]=r0 ;...09000048 a011</b>
5<b> 0x2000000001de81d6: mf ;...00000044 0000</b>
6 0x2000000001de81dc: nop.i 0x0;; ;...00040000
7 0x2000000001de81e0: mov r12=r33 ;...00600042 0021
8 0x2000000001de81e6: mov.ret b0=r35,0x2000000001de81e0
9 0x2000000001de81ec: mov.i ar.pfs=r34 ;...00aa0220
10 0x2000000001de81f0: mov r6=r32 ;...09300040 0021

这里我们可以看到在第四行第二次写操作被注解了一个显式内存屏障。通过使用 st.rel,即“存储释放”(store release),编译器确保第一次写操作在第二次写操作之前完成。这就完成了两边的约定,因为第一次写操作在第二次写操作之前发生。

st.rel 屏障是单向的——就像 ld.acq 一样。但是在第五行编译器设置了一个双向内存屏障。mf 指令,或者称为“内存栅栏”,是 Itanium 2 指令集中的完整栅栏。笔者认为是多余的。

内存屏障是特定于硬件的

本文不想针对所有内存屏障做一综述。这将是一件不朽的功绩。但是,重要的是认识到这些指令在不同的硬件体系中迥异。下面的指令是连续写操作在多处理 Intel Xeon 硬件上编译的结果。本文后面的所有汇编指令除非特殊声明否则都出自于 Intel Xeon。

复制代码
1 0x03f8340c: push %ebp ;...55
2 0x03f8340d: sub $0x8,%esp ;...81ec0800 0000
3 0x03f83413: mov $0x14c,%edi ;...bf4c0100 00
4 0x03f83418: movb $0x1,-0x505a72f0(%edi) ;...c687108d a5af01
5 0x03f8341f: mfence ;...0faef0
6 0x03f83422: mov $0x148,%ebp ;...bd480100 00
7 0x03f83427: mov $0x14d,%edx ;...ba4d0100 00
8 0x03f8342c: movsbl -0x505a72f0(%edx),%ebx ;...0fbe9a10 8da5af
9 0x03f83433: test %ebx,%ebx ;...85db
10 0x03f83435: jne 0x03f83460 ;...7529
11 0x03f83437: movl $0x1,-0x505a72f0(%ebp) ;...c785108d a5af01
12 0x03f83441: movb $0x0,-0x505a72f0(%edi) ;...c687108d a5af00
<b>13 0x03f83448: mfence ;...0faef0</b>
14 0x03f8344b: add $0x8,%esp ;...83c408
15 0x03f8344e: pop %ebp ;...5d

我们可以看到 x86 Xeon 在第 11、12 行执行两次 volatile 写操作。第二次写操作后面紧跟着 mfence 操作——显式的双向内存屏障。

下面的连续写操作基于 SPARC。

复制代码
1 0xfb8ecc84: ldub [ %l1 + 0x155 ], %l3 ;...e60c6155
2 0xfb8ecc88: cmp %l3, 0 ;...80a4e000
3 0xfb8ecc8c: bne,pn %icc, 0xfb8eccb0 ;...12400009
4 0xfb8ecc90: nop ;...01000000
5 0xfb8ecc94: st %l0, [ %l1 + 0x150 ] ;...e0246150
6 0xfb8ecc98: clrb [ %l1 + 0x154 ] ;...c02c6154
<b> 7 0xfb8ecc9c: membar #StoreLoad ;...8143e002</b>
8 0xfb8ecca0: sethi %hi(0xff3fc000), %l0 ;...213fcff0
9 0xfb8ecca4: ld [ %l0 ], %g0 ;...c0042000
10 0xfb8ecca8: ret ;...81c7e008
11 0xfb8eccac: restore ;...81e80000

我们看到在第五、六行存在两次 volatile 写操作。第二次写操作后面是一个 membar 指令——显式的双向内存屏障。

x86 和 SPARC 的指令流与 Itanium 的指令流存在一个重要区别。JVM 在 x86 和 SPARC 上通过内存屏障跟踪连续写操作,但是在两次写操作之间 没有放置内存屏障。另一方面,Itanium 的指令流在两次写操作之间存在内存屏障。为何 JVM 在不同的硬件架构之间表现不一?因为硬件架构都有自己的内 存模型,每一个内存模型有一套一致性保障。某些内存模型,如 x86 和 SPARC 等,拥有强大的一致性保障。另一些内存模型,如 Itanium、 PowerPC 和 Alpha,是一种弱保障。例如,x86 和 SPARC 不会重新排序连续写操作——也就没有必要放置内存屏障。Itanium、 PowerPC 和 Alpha 将重新排序连续写操作——因此 JVM 必须在两者之间放置内存屏障。JVM 使用内存屏障减少 Java 内存模型和硬件内存模型之间 的距离。

隐式内存屏障

显式屏障指令不是序列化内存操作的唯一方式。让我们再看一看 Counter 类这个例子。

复制代码
class Counter{
static int counter = 0;
public static void main(String[] _){
for(int i = 0; i < 100000; i++)
inc();
}
static synchronized void inc(){ counter += 1; }
}

Counter 类执行了一个典型的读 - 修改 - 写的操作。静态 counter 字段不是 volatile 的,因为所有三个操作必须要原子可见的。因此,inc 方法是 synchronized 修饰的。我们可以采用下面的命令编译 Counter 类并查看生成的汇编指令。Java 内存模型确保了 synchronized 区域的退出和 volatile 内存操作都是相同的可见性,因此我们应该预料到会有另一个内存屏障。

$ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes -XX:-UseBiasedLocking -XX:CompileCommand=print,Counter.inc Counter

复制代码
1 0x04d5eda7: push %ebp ;...55
2 0x04d5eda8: mov %esp,%ebp ;...8bec
3 0x04d5edaa: sub $0x28,%esp ;...83ec28
4 0x04d5edad: mov $0x95ba5408,%esi ;...be0854ba 95
5 0x04d5edb2: lea 0x10(%esp),%edi ;...8d7c2410
6 0x04d5edb6: mov %esi,0x4(%edi) ;...897704
7 0x04d5edb9: mov (%esi),%eax ;...8b06
8 0x04d5edbb: or $0x1,%eax ;...83c801
9 0x04d5edbe: mov %eax,(%edi) ;...8907
<b>10 0x04d5edc0: lock cmpxchg %edi,(%esi) ;...f00fb13e</b>
11 0x04d5edc4: je 0x04d5edda ;...0f841000 0000
12 0x04d5edca: sub %esp,%eax ;...2bc4
13 0x04d5edcc: and $0xfffff003,%eax ;...81e003f0 ffff
14 0x04d5edd2: mov %eax,(%edi) ;...8907
15 0x04d5edd4: jne 0x04d5ee11 ;...0f853700 0000
16 0x04d5edda: mov $0x95ba52b8,%eax ;...b8b852ba 95
17 0x04d5eddf: mov 0x148(%eax),%esi ;...8bb04801 0000
<b>18 0x04d5ede5: inc %esi ;...46</b>
19 0x04d5ede6: mov %esi,0x148(%eax) ;...89b04801 0000
20 0x04d5edec: lea 0x10(%esp),%eax ;...8d442410
21 0x04d5edf0: mov (%eax),%esi ;...8b30
22 0x04d5edf2: test %esi,%esi ;...85f6
23 0x04d5edf4: je 0x04d5ee07 ;...0f840d00 0000
24 0x04d5edfa: mov 0x4(%eax),%edi ;...8b7804
<b>25 0x04d5edfd: lock cmpxchg %esi,(%edi) ;...f00fb137</b>
26 0x04d5ee01: jne 0x04d5ee1f ;...0f851800 0000
27 0x04d5ee07: mov %ebp,%esp ;...8be5
28 0x04d5ee09: pop %ebp ;...5d

不出意外,synchronized 生成的指令数量比 volatile 多。第 18 行做了一次增操作,但是 JVM 没有显式插入内存屏障。相反,JVM 通过在 第 10 行和第 25 行 cmpxchg 的 lock 前缀一石二鸟。cmpxchg 的语义超越了本文的范畴。lock cmpxchg 不仅原子性执行写操作,也会刷新等待的读写操作。写操作现在将在所有后续内存操作之前完成。如果我们通过 java.util.concurrent.atomic.AtomicInteger 重构和运行 Counter,将看到同样的手段。

复制代码
import java.util.concurrent.atomic.AtomicInteger;
class Counter{
static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args){
for(int i = 0; i < 1000000; i++)
counter.incrementAndGet();
}
}

$ java -XX:+UnlockDiagnosticVMOptions -XX:PrintAssemblyOptions=hsdis-print-bytes -XX:CompileCommand=print,*AtomicInteger.incrementAndGet Counter

复制代码
1 0x024451f7: push %ebp ;...55
2 0x024451f8: mov %esp,%ebp ;...8bec
3 0x024451fa: sub $0x38,%esp ;...83ec38
4 0x024451fd: jmp 0x0244520a ;...e9080000 00
5 0x02445202: xchg %ax,%ax ;...6690
6 0x02445204: test %eax,0xb771e100 ;...850500e1 71b7
7 0x0244520a: mov 0x8(%ecx),%eax ;...8b4108
8 0x0244520d: mov %eax,%esi ;...8bf0
9 0x0244520f: inc %esi ;...46
10 0x02445210: mov $0x9a3f03d0,%edi ;...bfd0033f 9a
11 0x02445215: mov 0x160(%edi),%edi ;...8bbf6001 0000
12 0x0244521b: mov %ecx,%edi ;...8bf9
13 0x0244521d: add $0x8,%edi ;...83c708
<b>14 0x02445220: lock cmpxchg %esi,(%edi) ;...f00fb137</b>
15 0x02445224: mov $0x1,%eax ;...b8010000 00
16 0x02445229: je 0x02445234 ;...0f840500 0000
17 0x0244522f: mov $0x0,%eax ;...b8000000 00
18 0x02445234: cmp $0x0,%eax ;...83f800
19 0x02445237: je 0x02445204 ;...74cb
20 0x02445239: mov %esi,%eax ;...8bc6
21 0x0244523b: mov %ebp,%esp ;...8be5
22 0x0244523d: pop %ebp ;...5d

我们又一次在第 14 行看到了带有 lock 前缀的写操作。这确保了变量的新值(写操作)会在其他所有后续内存操作之前完成。

内存屏障能够避免

JVM 非常擅于消除不必要的内存屏障。通常 JVM 很幸运,因为硬件内存模型的一致性保障强于或者等于 Java 内存模型。在这种情况下,JVM 只是简单地插 入一个 no op 语句,而不是真实的内存屏障。例如,x86 和 SPARC 内存模型的一致性保障足够强壮以消除读 volatile 变量时所需的内存屏障。还记得在 Itanium 上两次读操作之间的显式单向内存屏障吗?x86 上的 Dekker 算法中连续 volatile 读操作的汇编指令之间没有任何内存屏障。

x86 平台上共享内存的连续读操作。

复制代码
1 0x03f83422: mov $0x148,%ebp ;...bd480100 00
2 0x03f83427: mov $0x14d,%edx ;...ba4d0100 00
<b> 3 0x03f8342c: movsbl -0x505a72f0(%edx),%ebx ;...0fbe9a10 8da5af</b>
4 0x03f83433: test %ebx,%ebx ;...85db
5 0x03f83435: jne 0x03f83460 ;...7529
6 0x03f83437: movl $0x1,-0x505a72f0(%ebp) ;...c785108d a5af01
7 0x03f83441: movb $0x0,-0x505a72f0(%edi) ;...c687108d a5af00
8 0x03f83448: mfence ;...0faef0
9 0x03f8344b: add $0x8,%esp ;...83c408
10 0x03f8344e: pop %ebp ;...5d
11 0x03f8344f: test %eax,0xb78ec000 ;...850500c0 8eb7
12 0x03f83455: ret ;...c3
13 0x03f83456: nopw 0x0(%eax,%eax,1) ;...66660f1f 840000
<b>14 0x03f83460: mov -0x505a72f0(%ebp),%ebx ;...8b9d108d a5af</b>
15 0x03f83466: test %edi,0xb78ec000 ;...853d00c0 8eb7

第三行和第十四行存在 volatile 读操作,而且都没有伴随内存屏障。也就是说,x86 和 SPARC 上的 volatile 读操作的性能下降对于代码的优 化影响很小——指令本身和常规读操作一样。

单向内存屏障本质上比双向屏障性能要好一些。JVM 在确保单向屏障即可的情况下会避免使用双向屏障。本文的第一个例子展示了这点。Itanium 平台上的 连续两次读操作被插入单向内存屏障。如果读操作插入显式双向内存屏障,程序仍然正确,但是延迟比较长。

动态编译

静态编译器在构建阶段决定的一切事情,在动态编译器那里都可以在运行时决定,甚至更多。更多信息意味着存在更多机会可以优化。例如,让我们看看 JVM 在单 处理器运行时如何对待内存屏障。以下指令流来自于通过 Dekker 算法实现两次连续 volatile 写操作的运行时编译。程序运行于 x86 硬件上的单处理器模式中的 VMWare 工作站镜像。

复制代码
1 0x017b474c: push %ebp ;...55
2 0x017b474d: sub $0x8,%esp ;...81ec0800 0000
3 0x017b4753: mov $0x14c,%edi ;...bf4c0100 00
4 0x017b4758: movb $0x1,-0x507572f0(%edi) ;...c687108d 8aaf01
5 0x017b475f: mov $0x148,%ebp ;...bd480100 00
6 0x017b4764: mov $0x14d,%edx ;...ba4d0100 00
7 0x017b4769: movsbl -0x507572f0(%edx),%ebx ;...0fbe9a10 8d8aaf
8 0x017b4770: test %ebx,%ebx ;...85db
9 0x017b4772: jne 0x017b4790 ;...751c
<b>10 0x017b4774: movl $0x1,-0x507572f0(%ebp) ;...c785108d 8aaf01<br></br>11 0x017b477e: movb $0x0,-0x507572f0(%edi) ;...c687108d 8aaf00</b>
12 0x017b4785: add $0x8,%esp ;...83c408
13 0x017b4788: pop %ebp ;...5d

在单处理器系统上,JVM 为所有内存屏障插入了一个 no op 指令,因为内存操作已经序列化了。每一个写操作(第 10、11 行)后面都跟着一个屏障。JVM 针对原子条件式做了类似的优化。下面的指令流来自于同一 个 VMWare 镜像的 AtomicInteger.incrementAndGet 动态编译结果。

复制代码
1 0x036880f7: push %ebp ;...55
2 0x036880f8: mov %esp,%ebp ;...8bec
3 0x036880fa: sub $0x38,%esp ;...83ec38
4 0x036880fd: jmp 0x0368810a ;...e9080000 00
5 0x03688102: xchg %ax,%ax ;...6690
6 0x03688104: test %eax,0xb78b8100 ;...85050081 8bb7
7 0x0368810a: mov 0x8(%ecx),%eax ;...8b4108
8 0x0368810d: mov %eax,%esi ;...8bf0
9 0x0368810f: inc %esi ;...46
10 0x03688110: mov $0x9a3f03d0,%edi ;...bfd0033f 9a
11 0x03688115: mov 0x160(%edi),%edi ;...8bbf6001 0000
12 0x0368811b: mov %ecx,%edi ;...8bf9
13 0x0368811d: add $0x8,%edi ;...83c708
<b>14 0x03688120: cmpxchg %esi,(%edi) ;...0fb137</b>
15 0x03688123: mov $0x1,%eax ;...b8010000 00
16 0x03688128: je 0x03688133 ;...0f840500 0000
17 0x0368812e: mov $0x0,%eax ;...b8000000 00
18 0x03688133: cmp $0x0,%eax ;...83f800
19 0x03688136: je 0x03688104 ;...74cc
20 0x03688138: mov %esi,%eax ;...8bc6
21 0x0368813a: mov %ebp,%esp ;...8be5
22 0x0368813c: pop %ebp ;...5d

注意第 14 行的 cmpxchg 指令。之前我们看到编译器通过 lock 前缀把该指令提供给处理器。由于缺少 SMP,JVM 决定避免这种成本——与静态编译有些不同。

结束语

内存屏障是多线程编程的必要装备。它们形式多样,某些是显式的,某些是隐式的。某些是双向的,某些是单向的。JVM 利用这些形式在所有平台中有效地支持 Java 内存模型。我希望本文能够帮助经验丰富的 JVM 开发人员了解一些代码在底层如何运行的知识。

参考书目

关于作者

Dennis Byrne DRW Trading (一家自营证券投资公司和流通量供应商)的一名高级软件 工程师。他是一名作家、演说家和开源社区的活跃成员。

查看英文原文 Memory Barriers and JVM Concurrency


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-04-21 01:1026791
用户头像

发布了 501 篇内容, 共 248.3 次阅读, 收获喜欢 57 次。

关注

评论

发布
暂无评论
发现更多内容

开源即时通讯IM框架MobileIMSDK的微信小程序端开发快速入门

JackJiang

TiDB损坏多副本之有损恢复处理方法

TiDB 社区干货传送门

集群管理 6.x 实践 TiKV 底层架构

干货分享|金融机构如何通过标签画像实现精细化客户运营?

索信达控股

文盘Rust -- 用Tokio实现简易任务池

TiDB 社区干货传送门

开发语言

知行合一!AI大模型与算法二三事

深数

深度学习 科普 数字化 NLP 大模型 LLM

堡垒机厂商都是大企业吗?你比较推荐哪家?

行云管家

网络安全 等级保护

Flink MongoDB CDC 在 XTransfer 的生产实践|Flink CDC 专题

Apache Flink

大数据 flink 实时计算

快手基于 Apache Flink 的实时数仓建设实践

Apache Flink

大数据 flink 实时计算

APP频繁改版惹人烦?火山引擎VeDI来帮忙

字节跳动数据平台

数字化 企业数字化 企业号 4 月 PK 榜 APP改版

精选2023年大厂高频Java面试真题集锦(含答案),面试一路开挂

程序知音

java面试 java架构 Java进阶 后端技术 Java面试八股文

“信创”滚滚而来,私有化或将迎来第二春

WorkPlus

阿里大佬倾情力荐:Java全线成长宝典,从P5到P8一应俱全

三十而立

Java java面试

瓴羊Quick BI国产数字化智能工具口碑怎么样?30天免费试用

小偏执o

GitHub已开源—在国内外都被称为分布式理论+实践的巅峰之作

做梦都在改BUG

Java 数据库 分布式 系统设计 设计数据密集型应用

TiCDC 源码阅读(五)TiCDC 对 DDL 的处理和 Filter 解析

TiDB 社区干货传送门

TiCDC 源码阅读(七) TiCDC Sorter 模块揭秘

TiDB 社区干货传送门

企业数字化升级迫在眉睫,瓴羊Quick BI工具应运而生

夏日星河

四种常见服务限流算法解析

做梦都在改BUG

漫谈 ChatGPT 与问答式 BI

观远数据

数据分析 BI ChatGPT

ByteBase是什么,他怎么和tidb结合提高工作效率的

TiDB 社区干货传送门

实践案例

【福利】ChatGPT免费体验期延长,商用版正式开启预约!

WorkPlus

瓴羊Quick BI连续入选魔力象限ABI报告,实至名归

流量猫猫头

一文彻底搞懂Raft算法,看这篇就够了!!!

做梦都在改BUG

NFT交易平台商城系统开发技术

薇電13242772558

NFT

TiCDC 源码阅读(六)TiCDC Puller 模块介绍

TiDB 社区干货传送门

tiup cluster display 执行流程代码详解

TiDB 社区干货传送门

实践案例 集群管理 故障排查/诊断 安装 & 部署

5 大手段,打造单一可信源代码托管平台|极狐GitLab DevSecOps 助力 SLSA 落地之源代码篇

极狐GitLab

DevOps DevSecOps 源代码 安全审计 SLSA

详解 Flink Catalog 在 ChunJun 中的实践之路

袋鼠云数栈

flink

Stable Diffusion:一种新型的深度学习AIGC模型

蓝海大脑GPU

堡垒机主流品牌有哪些?如何选择?

行云管家

堡垒机 IT运维

Redis崩吗?来一起搞定 Redis 实践中的常见问题!

Steven

redis

内存屏障与JVM并发_Java_Dennis Byrne_InfoQ精选文章