HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

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

关注

评论

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

从技术到服务,小鹅通成功的「底层逻辑」是什么?

ToB行业头条

Flutter 安卓 Platform 与 Dart 端消息通信方式 Channel 源码解析

工匠若水

flutter android 8月日更

filecoin挖矿教程?filecoin挖矿收益如何?

区块链 分布式存储 IPFS filecoin挖矿 filecoin收益

如何认知新技术?区块链技术和应用

百度开发者中心

区块链 最佳实践 方法论

OceanBase 源码解读(三)分区的一生

OceanBase 数据库

数据库 分布式数据库 oceanbase OceanBase 开源 OceanBase 社区版

【DPDK工程师手册】 —— 官方文档,最新视频,开源项目,论文,大厂内部ppt,知名工程师一览表

奔着腾讯去

Linux DPDK VPP

Python代码阅读(第18篇):变形词判断

Felix

Python 编程 Code Programing 阅读代码

01. 你身边的AI

Databri_AI

人工智能

正经人一辈子都用不到的 JavaScript 方法总结 (二)

编程三昧

JavaScript 大前端 8月日更

深入了解现代web浏览器(第一部分)

GKNick

每天学习 10 个实用Javascript代码片段(六)

devpoint

mathjs 加密函数 随机数 8月日更

微信业务架构图-作业

Geek_a772a7

AI巨头们建造的“新世界”,进展如何?

脑极体

中国做ipfs公司排名?挖ipfs排名前三的公司是?ipfs矿机哪家最好?

中国做ipfs公司排名 ipfs矿机哪家最好 挖ipfs排名前三的公司是

区块链技术如何在涉诉信访中显身手

CECBC

强化学习中,Q-Learning与Sarsa的差别有多大?

行者AI

强化学习

安卓工控主板通信接口有哪些呢?

双赞工控

安卓主板 工控主板

你真的了解二叉树吗?(手撕算法篇)

有道技术团队

二叉树 网易有道

python通过PyQt5实现登录界面

Python研究者

8月日更

网络安全小白别拜师了,求人不如求己

网络安全学海

黑客 网络安全 信息安全 渗透测试 安全漏洞

比特币矿池如何触底反弹?比特币矿企的未来出路在哪里?

CECBC

图像分类-cifar100 实验研究

毛显新

人工智能 神经网络 tensorflow 图像识别 keras

Baetyl推动边云融合 点亮智能物联网

百度大脑

人工智能 开源

HTTP接口测试基础【FunTester框架教程】

FunTester

自动化测试 教程 接口测试 测试框架 FunTester

仓储执行系统(WES)

申扬科技

WCS wms WES 仓储执行系统

学生管理系统(作业)

Geek_a772a7

久等了!【Innovation 2021】网易应用创新开发者大赛正式开赛!

网易云信

开源 架构 开发者 网易 语言 & 开发

Filecoin价格今日行情:filecoin价格还能怎么走?

区块链 分布式存储 IPFS filecoin价格 filecoin行情

新思科技软件组成分析解决方案获得Forrester Wave认可

InfoQ_434670063458

新斯科技 Forrester 软件组成分析

业界首个高性能交互式自动标注工具——EISeg正式开源!

百度大脑

人工智能 开源

☕【Java技术指南】「OpenJDK专题」想不想编译属于你自己的JDK呢?(Windows10环境)

洛神灬殇

Java jdk Openjdk 8月日更

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