写点什么

内存屏障与 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:1027091
用户头像

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

关注

评论

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

探索网络世界的核心:TCPIP协议四层模型解析

做梦都在改BUG

Java 计算机网络 网络协议 TCP/IP

面试造火箭?GitHub 飙升“2023(Java 岗)面试真题汇总”转载 40 万

三十而立

云原生:驱动企业数字化新模式

北京好雨科技有限公司

云原生 数字化 rainbond 企业号 4 月 PK 榜

联想超融合加入龙蜥社区,多产品完成与 Anolis OS 适配

OpenAnolis小助手

开源 操作系统 龙蜥社区 龙腾计划 联想超融合

华为云开源项目OpenTiny的TinyNG组件库的设计理念是什么?

英勇无比的消炎药

前端 开源项目 OpenTiny UI组件库

[翻译]反生产力宣言

宇宙之一粟

人生 时间管理 高效能

一站式开发平台 加速企业数字化发展

力软低代码开发平台

KubeVela:云原生应用和平台工程之路

阿里巴巴云原生

阿里云 开源 云原生 KubeVela

真下饭!字节技术官DDD(领域驱动设计)手册,拆解业务代码首选

做梦都在改BUG

Java 架构 领域驱动设计 DDD

构建系列之前端脚手架vite

江湖修行

Vue vite cli

软件测试/测试开发丨H5性能分析实战

测试人

软件测试 性能测试 自动化测试 H5 W3C

喜讯!索信达荣获CCSA TC601年度“优秀成员单位”

索信达控股

阿里巴巴内网 Java 面试 2000 题解析(2023 最新版

三十而立

华为云开源项目OpenTiny的TinyCLI是什么时候开源的?

英勇无比的消炎药

前端 开源项目 cli UI组件库

如何用 YonBuilder 构建线索管理应用?

YonBuilder低代码开发平台

阿里P8架构师20年经验总结成微服务设计企业架构转型之道笔记

程序知音

Java 微服务 java架构 Java进阶 后端技术

有关TCP协议,这是我看过讲的最清楚的一篇文章了!

三十而立

详解事务模式和Lua脚本,带你吃透Redis 事务

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 4 月 PK 榜

软件测试/测试开发丨如何开始webView 性能测试

测试人

软件测试 性能测试 自动化测试 测试开发

如何过好4000周:关于重新校准人生时间的建议

宇宙之一粟

时间管理

2023年最强手机远程控制横测:ToDesk、向日葵、Airdroid三款APP免Root版本

陈橘又青

远程连接

Apifox:API 接口自动化测试完全指南

Apifox

测试 自动化测试 测试工具 接口工具免费 免费工具

华为云发布多项场景化解决方案助力制造业企业加速上云

IT科技苏辞

读懂一个项目的研发效能 之 项目人效

思码逸研发效能

研发效能 功能更新

面试官:说一说mysql的varchar字段最大长度?

程序员小毕

MySQL 数据库 程序员 面试 架构师

智能汽车主题 Meetup 线下报名开启!IoTDB X EMQ 为智慧车联和智能制造打造数据基础设施平台

Apache IoTDB

智能汽车 IoTDB Apache IoTDB

狂刷《Java 权威面试指南(阿里版)》,冲击“金三银四”有望了

三十而立

不想做架构师的Gopher不是好程序员

王中阳Go

Docker 高效工作 学习方法 面试题 Go 语言

【亲测有效】30 岁测试工程师的 12 个破除内卷技能!

禅道项目管理

职场 互联网人 敏捷测试 测试工程师

MobTech MobPush|推送的下发逻辑是什么样的

MobTech袤博科技

制造企业如何解决数据分散和管理困难的问题,实现数字化转型?

IT科技苏辞

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