写点什么

Java 6 中的线程优化真的有效么?——第二部分

2008 年 10 月 29 日

本文的第一部分中,我们通过一个单一线程的基准,比较了同步的StringBuffer 和非同步的StringBuilder 之间的性能。从最初的基准测试结果来看,偏向锁提供了最佳的性能,比其他的优化方式更有效。测试的结果似乎表明获取锁是一项昂贵的操作。但是在得出最终的结论之前,我决定先对结果进行检验:我请我的同事们在他们的机器上运行了这个测试。尽管大多数结果都证实了我的测试结果,但是有一些结果却完全不同。在本文的第二部分中,我们将更深入地看一看用于检验测试结果的技术。最后我们将回答现实中的问题:为什么在不同的处理器上的锁开销差异如此巨大?

基准测试中的陷阱

通过一个基准测试,尤其是一个“小规模基准测试”(microbenchmark),来回答这个问题是非常困难的。多半情况下,基准测试会出现一些与你期望测量的完全不同的情景。即使当你要测量影响这个问题的因素时,结果也会被其他的因素所影响。有一点在这个实验开始之初就已经很明确了,即这个基准测试需要由其他人全面地进行审查,这样我才能避免落入报告无效基准测试数据的陷阱中。除了其他人的检查以外,我还使用了一些工具和技术来校验结果,这些我会在下面的几节中谈到。

结果的统计处理

大多数计算机所执行的操作都会在某一固定的时间内完成。就我的经验而言,我发现即使是那些不确定性的操作,在大多数条件下基本上也能在固定的时间内完成。正是根据计算的这种特性,我们可以使用一种工具,它通过测量让我们了解事情何时开始变得不正常了。这样的工具是基于统计的,其测量结果会有些出入。这就是说,即使看到了一些超过正常水平的报告值,我也不会做过多过的解释的。原因是这样的,如果我提供了指令数固定的CPU,而它并没有在相对固定的时间内完成的话,就说明我的测量受到了一些外部因素的影响。如果测试结果出现了很大的异常,则意味着我必须找到这个外部的影响进而解决它。

尽管这些异常效果会在小规模基准测试中被放大,但它不至于会影响大规模的基准测试。对于大规模的基准测试来说,被测量的目标应用程序的各个方面会彼此产生干扰,这会带来一些异常。但是异常仍然能够提供一些很有益的信息,可以帮助我们对干扰级别作出判断。在稳定的负荷下,我并不会对个别异常情况感到意外;当然,异常情况不能过多。对于那些比通常结果大一些或小一些的结果,我会观察测试的运行情况,并将它视为一种信号:我的基准测试尚未恰当地隔离或者设置好。这样对相同的测试进行不同的处理,恰恰说明了全面的基准测试与小规模基准测试之间的不同。

最后一点,到此为止仍然不能说明你所测试的就是你所想的。这至多只能说明,对于最终的问题,这个测试是最有可能是正确的。

预热方法的缓存

JIT 会编译你的代码,这也是众多影响基准测试的行为之一。Hotspot 会频繁地检查你的程序,寻找可以应用某些优化的机会。当找到机会后,它会要求 JIT 编译器重新编译问题中的某段代码。此时它会应用一项技术,即当前栈替换(On Stack Replacement,OSR),从而切换到新代码的执行上。执行 OSR 时会对测试产生各种连锁影响,包括要暂停线程的执行。当然,所有这样的活动都会干扰到我们的基准测试。这类干扰会使测试出现偏差。我们手头上有两款工具,可以帮助我们标明代码何时受到 JIT 的影响了。第一个当然是测试中出现的差异,第二个是 -XX:-PrintCompilation 标记。幸运的是,如果不是所有的代码在测试的早期就进行 JIT 化处理,那么我们可以将它视为另外一种启动时的异常现象。我们需要做的就是在开始测量前,先不断地运行基准测试,直到所有代码都已经完成了 JIT 化。这个预热的阶段通常被称为“预热方法的缓存 ”。

大多数 JVM 会同时运行在解释的与本机的模式中。这就是所谓的混合模式执行。随着时间的流逝,Hotspot 和 JIT 会根据收集的信息将解释型代码转化为本机代码。Hotspot 为了决定应该使用哪种优化方案,它会抽样一些调用和分支。一旦某个方法达到了特定的阈值后,它会通知 JIT 生成本机代码。这个阈值可以通过 -XX:CompileThreshold 标记来设定。例如,设定 -XX:CompileThreshold=10000,Hotspot 会在代码被执行 10,000 次后将它编译为本机代码。

堆管理

下一个需要考虑的是垃圾收集,或者更广为人知的名字—堆管理。在任何应用程序执行的过程中,都会定期地发生很多种内存管理活动。它们包括:重新划分栈空间大小、回收不再被使用的内存、将数据从一处移到另一处等等。所有这些行为都导致 JVM 影响你的应用程序。我们面对的问题是:基准测试中是否需要将内存维护或者垃圾回收的时间包括进来?问题的答案取决于你要解决的问题的种类。在本例中,我只对获取锁的开销感兴趣,也就是说,我必须确保测试中不能包含垃圾回收的时间。这一次,我们又能够通过异常的现象来发现影响测试的因素,一旦出现这种问题,垃圾回收都是一个可能的怀疑对象。明确问题的最佳方式是使用 -verbose:gc 标志,开启 GC 的日志功能。

在这个基准测试中,我做了大量的 String、StringBuffer 和 StringBuilder 操作。在每次运行的过程中大概会创建 4 千万个对象。对于这样一种数量级的对象群来说,垃圾回收毫无疑问会成为一个问题。我使用两项技术来避免。第一,提高堆空间的大小,防止在一个迭代中出现垃圾回收。为此,我利用了如下的命令行:

复制代码
>java -server -XX:+EliminateLocks -XX:+UseBiasedLocking -verbose:gc -XX:NewSize=1500m -XX:SurvivorRatio=200000 LockTest

然后,加入清单 1 的代码,它为下一次迭代准备好堆空间。

复制代码
System.gc();
Thread.sleep(1000);

清单 1. 运行 GC,然后进行短暂的休眠。

休眠的目的在于给垃圾回收器充分的时间,在释放其他线程之后完成工作。有一点需要注意:如果没有 CPU 任何活动,某些处理器会降低时钟频率。因此,尽管 CPU 时钟会自旋等待,但引入睡眠的同时也会引入延迟。如果你的处理器支持这种特性,你可能必须要深入到硬件并且关闭掉“节能”功能才行。

前面使用的标签并不能阻止 GC 的运行。它只表示在每一次测试用例中只运行一次 GC。这一次的暂停非常小,它产生的开销对最终结果的影响微乎其微。对于我们这个测试来说,这已经足够好了。

偏向锁延迟

还有另外一种因素会对测试结果产生重要的影响。尽管大多数优化都会在测试的早期发生,但是由于某些未知的原因,偏向锁只发生在测试开始后的三到四秒之后。我们又要重述一遍,异常行为再一次成为判断是否存在问题的重要标准了。-XX:+TraceBiasedLocking 标志可以帮助我们追踪这个问题。还可以延长预热时间来克服偏向锁导致的延迟。

Hotspot 提供的其他优化

Hotspot 不会在完成一次优化后就停止对代码的改动。相反,它会不断地寻找更多的机会,提供进一步的优化。对于锁来说,由于很多优化行为违反了 Java 存储模型中描述的规范,所以它们是被禁止的。然而,如果锁已经被 JIT 化了,那么这些限制很快就会消失。在这个单线程化的基准测试中,Hotspot 可以非常安全地将锁省略掉。这样就会为其他的优化行为打开大门;比如方法内联、提取循环不变式以及死代码的清除。

如果仔细思考下面的代码,可以发现 A 和 B 都是不变的,我们应该把它抽取出来放到循环外面,并引入第三个变量,这样可以避免重复的计算,正如清单 3 中所示的那样。通常,这都是程序员的事情。但是 Hotspot 可以识别出循环不变式并把它们抽取到循环体外面。因此,我们可以把代码写得像清单 2 那样,但是它执行时其实更类似于清单 3 的样子。 ```

int A = 1;
int B = 2;
int sum = 0;
for (int i = 0; i < someThing; i++) sum += A + B;

复制代码
** 清单 2 循环中包含不变式 **

int A = 1;
int B = 2;
int sum = 0;
int invariant = A + B;
for (int i = 0; i < someThing; i++) sum += invariant;

复制代码
** 清单 3 不变式已抽取到循环之外 **

这些优化真的应该允许么?还是我们应该做一些事情防止它的发生?这个有待商榷。但至少,我们应该知道是否应用了这些优化。我们绝对要避免“死代码消除”这种优化的出现,否则它会彻底扰乱我们的测试!Hotspot 能够识别出我们没有使用 concatBuffer 和 concatBuilder 操作的结果。或者可以说,这些操作没有边界效应。因此没有任何理由执行这些代码。一旦代码被标识为已“死亡”,JIT 就会除去它。好在我的基准测试迷惑了 Hotspot,因此它并没有识别出这种优化,至少目前还没有。

如果由于锁的存在而抑制了内联,反之没有锁就可能出现内联,那么我们要确保在测试结果中没有包含额外的方法调用。现在可以用到的一种技术是引入一个接口(清单 4)来迷惑 Hotspot。

复制代码
<span color="#7f0055"><strong>public</strong></span><strong> <span color="#7f0055" id="u_wg474">interface</span></strong><span color="#000000" id="u_wg475">Concat {</span>
String concatBuffer(String s1, String s2, String s3);
String concatBuilder(String s1, String s2, String s3);
<span color="#7f0055"><strong><br></br>public</strong></span><strong> <span color="#7f0055">class</span></strong> <span color="#000000">LockTest</span> <span color="#7f0055"><strong>implements</strong></span> Concat {
...}

清单 4 使用接口防止方法内联

防止内联的另一种方法是使用命令行选项 -XX:-Inline。我已经验证,方法内联并没有给基准测试的报告带来任何不同。

执行栈输出

最后,请看下面的输出结果,它使用了下面的命令行标识。

复制代码
>java -server -XX:+DoEscapeAnalysis -XX:+PrintCompilation -XX:+EliminateLocks -XX:+UseBiasedLocking -XX:+TraceBiasedLocking LockTest

图 1 基准测试的执行栈输出

JVM 默认会启动 12 个线程,包括:主线程、对象引用处理器、Finalize、Attach 监听器等等。上图中第一个灰色段显示的是这些线程的对齐,它们可以使用偏向锁(注意所有地址都以 00 结尾)。你尽管忽略可以忽略它们。接下来的黄色段包含了已编译方法的信息。我们看一下第 5 行和 12 行,能够发现它们都标记了一个额外的“s”。表 1 的信息告诉我们这些方法都是同步的。包含了“%”的各行已经使用了 OSR。红色的行是偏向锁被激活的地方。最底下的蓝绿色框是基准测试开始计时的地方。从记录基准测试开始时间的输出中可以看到,所有编译都已经发生了。这说明前期的预热阶段足够长了。如果你想了解日志输出规范的更多细节,可以参考这个页面和这篇文章

表1 编译示例码

单核系统下的结果

尽管我的多数同事都在使用Intel Core 2 Duo 处理器,但还是有一小部分人使用陈旧的单核机器。在这些陈旧的机器上,StringBuffer 基准测试的结果和StringBuilder 实现的结果几乎相同。由于产生这种不同可能是多种因素使然,因此我需要另外一个测试,尝试忽略尽可能多的可能性。最好的选择是,在BIOS 中关闭Core 2 Duo 中的一个核,然后重新运行基准测试。运行的结果如图2 所示。

图2 单核系统的性能

在多核环境下运行的时候,关闭了三种优化行为后获得了一个基准值。这次,StringBuilder 又保持了平稳的吞吐量。更有趣的是,尽管 StringBuffer 比StringBuilder 要稍慢,但是在多核平台下,StringBuffer 的性能更接近于StringBuilder。从这个测试开始我们将一步步勾勒出基准测试的真实面目。

在多核的世界中,线程间共享数据的现实呈现出一种全新的面貌。所有现代的CPU 必须使用本地存储的缓存,将获取指令和数据的延迟降到最低。当我们使用锁的时候,会导致一次存储关卡(Barrier)被插入到执行路径中。存储关卡像一个信号,它通知CPU 此时必须和其他所有的CPU 进行协调,以此获得最新的数值。为了完成这个任务,CPU 之间将要彼此通讯,从而导致每个处理器暂定当前正在运行的应用程序线程。这个过程要花多少时间已经成了CPU 存储模型的指标之一。越是保守的存储模型,越是线程安全的,但是它们在协调各个处理器核的时候也要花费更多的时间。在Core 2 Duo 上,第二个核将固定的运行基准从3731ms 提高到了6574ms,或者说增加了176%。很明显,Hotspot 所提供的任何帮助都能明显改进我们的应用程序的总体性能。

逸出分析真的起作用了么?

现在,还有一种优化很明显会起作用,但是我们还没有考虑,它就是锁省略。锁省略是最近才实现的技术,而且它依赖于逸出分析,后者是一种Profiling 技术,其自身也是刚刚才实现的。为了稳妥一些,各公司和组织都宣称这些技术只有在有限的几种情况下才起作用。比如,在一个简单的循环里,对一个局部变量执行递增,且该操作被包含在一个同步块内,由一个局部的锁保护着。这种情况下逸出分析是起作用的[ http://blog.nirav.name/2007_02_01_archive.html ]。同时它在 Mont Carlo 的 Scimark2 基准测试中可以工作(参见 [ http://math.nist.gov/scimark2/index.html ])。

将逸出分析包含在测试中

那么,为什么逸出分析可以用于上述的情况中,却不能用于我们的基准测试中?我曾经尝试过将 StringBuffer 和 StringBuilder 的部分方法进行内联。我也修改过代码,希望可以强制逸出分析运行。我想看到锁最终被忽略,而性能可以获得大幅提升。老实说,处理这个基准测试的过程既困惑,又让人倍感挫折。我必须无数次地在编辑器中使用 ctrl-z,以便恢复到前面一个我认为逸出分析应该起作用的版本,但是却不知由于什么原因,逸出分析却突然不起作用了。有时,锁省略却又会莫名其妙地出现。

最后,我认识到激活锁省略似乎和被锁对象的数据大小有关系。你运行清单 2 的代码就会看到这一点。正如你所看到的,无论运行多少次,结果都毫无区别,这说明 DoEscapeAnalysi 没有产生影响。

复制代码
>java -server -XX:-DoEscapeAnalysis EATest
thread unsafe: 941 ms.
thread safe: 1960 ms.
Thread safety overhead: 208%
>java -server -XX:+DoEscapeAnalysis EATest
thread unsafe: 941 ms.
thread safe: 1966 ms.
Thread safety overhead: 208%

在下面的两次运行中,我移除了 ThreadSafeObject 类中一个没有被用过的域。如你所见,当开启了逸出分析,所有性能有了很大的提高。

复制代码
>java -server -XX:-DoEscapeAnalysis EATest
thread unsafe: 934 ms.
thread safe: 1962 ms.
Thread safety overhead: 210%
>java -server -XX:+DoEscapeAnalysis EATest
thread unsafe: 933 ms.
thread safe: 1119 ms.
Thread safety overhead: 119%

逸出分析的数目在 Windows 和 Linux 上都能看到。然而在 Mac OS X 上,即使有额外未被使用的变量也不会有任何影响,任何版本的基准测试的结果都是 120%。这让我不由地相信在 Mac OS X 上有效性的范围比其他系统更广泛。我猜测这是由于它的实现比较保守,根据不同条件(比如锁对象数据大小和其他 OS 特定的特性)及早地关掉了它。

结论

当我刚开始这个实验,解释应用各种锁优化的 Hotspot 的有效性的时候,我估计它将花费我几个小时的时间,最终这会丰富我的 blog 的内容。但是就像其他的基准测试一样,对结果进行验证和解释的过程最终耗费了几周的时间。同样,我也与很多专家进行合作,他们分别花费了大量时间检查结果,并发表他们的见解。即使在这些工作完成以后,仍然很难说哪些优化起作用了,而哪些没有起作用。尽管这篇文章引述了一组测试结果,但它们是特定我的硬件和系统的。大家可以考虑是否能在自己的系统上看到相同类型的测试结果。另外,我最初认为这不过是个小规模基准测试,但是后来它逐渐既要满足我,也要满足所有审核代码的人,而且去掉了 Hotspot 不必要的优化。总之,这个实验的复杂度远远地超出了我的预期。

如果你需要在多核机器上运行多线程的应用程序,并且关心性能,那么很明显,你需要不断地更新所使用的 JDK 到最新版本。很多(但不是全部)前面的版本的优化都可以在最新的版本中获得兼容。你必须保证所有的线程优化都是激活的。在 JDK 6.0 中,它们默认是激活的。但是在 JDK 5.0 中,你需要在命令行中显式地设置它们。如果你在多核机器上运行单线程的应用程序,就要禁用除第一个核以外所有核的优化,这样会使应用程序运行得更快。

在更低级的层面上,单核系统上锁的开销远远低于双核处理器。不同核之间的协调,比如存储关卡语义,通过关掉一个核运行的测试结果看,很明显会带来系统开销。我们的确需要线程优化,以此降低这一开销。幸运的是,锁粗化和(尤其是)偏向锁对于基准测试的性能确实有明显的影响。我也希望逸出分析与锁省略一起更能够做到更好,产生更多的影响。这项技术会起作用,可只是在很少的情况下。客观地说,逸出分析仍然还处于它的初级阶段,还需要大量的时间才能变得成熟。

最后的结论是,最权威的基准测试是让你的应用程序运行在自己的系统上。当你的多线程应用的性能没有符合你的期望的时候,这篇文章能够为你提供了一些思考问题的启示。而这就是此文最大的价值所在。

关于 Jeroen Borgers

Jeroen Borger 是 Xebia 的资深咨询师。Xebia 是一家国际 IT 咨询与项目组织公司,专注于企业级 Java 和敏捷开发。Jeroen 帮助他的客户攻克企业级 Java 系统的性能问题,他同时还是 Java 性能调试课程的讲师。他在从 1996 年开始就可以在不同的 Java 项目中工作,担任过开发者、架构师、团队 lead、质量负责人、顾问、审核员、性能测试和调试员。他从 2005 年开始专注于性能问题。

鸣谢

没有其他人的鼎力相助,是不会有这篇文章的。特别感谢下面的朋友:

Dr. Cliff Click,原 Sun 公司的 Server VM 主要架构师,现工作在 Azul System;他帮我分析,并提供了很多宝贵的资源。

Kirk Pepperdine,性能问题的权威,帮助我编辑文章。

David Dagastine,Sun JVM 性能组的 lead,他为我解释了很多问题,并把我引领到正确的方向。

我的很多 Xebia 的同事帮我进行了基准测试。

资源

Java concurrency in practice, Brian Goetz et all.

Java theory and practice: Synchronization optimizations in Mustang ,

Did escape analysis escape from Java 6

Dave Dice’s Weblog

Java SE 6 Performance White Paper

清单 1.

public class LockTest {

private static final int MAX = 20000000; // 20 million

public static void main(String[] args) throws InterruptedException {

// warm up the method cache

for (int i = 0; i < MAX; i++) {

concatBuffer(“Josh”, “James”, “Duke”);

concatBuilder(“Josh”, “James”, “Duke”);

}

System.gc();

Thread.sleep(1000);

long start = System.currentTimeMillis();

for (int i = 0; i < MAX; i++) {

concatBuffer(“Josh”, “James”, “Duke”);

}

long bufferCost = System.currentTimeMillis() - start;

System.out.println(“StringBuffer: " + bufferCost + " ms.”);

System.gc();

Thread.sleep(1000);

start = System.currentTimeMillis();

for (int i = 0; i < MAX; i++) {

concatBuilder(“Josh”, “James”, “Duke”);

}

long builderCost = System.currentTimeMillis() - start;

System.out.println(“StringBuilder: " + builderCost + " ms.”);

System.out.println("Thread safety overhead of StringBuffer: "

+ ((bufferCost * 10000 / (builderCost * 100)) - 100) + “%\n”);

}

public static String concatBuffer(String s1, String s2, String s3) {

StringBuffer sb = new StringBuffer();

sb.append(s1);

sb.append(s2);

sb.append(s3);

return sb.toString();

}

public static String concatBuilder(String s1, String s2, String s3) {

StringBuilder sb = new StringBuilder();

sb.append(s1);

sb.append(s2);

sb.append(s3);

return sb.toString();

}

}

清单 2.

public class EATest {

private static final int MAX = 200000000; // 200 million

public static final void main(String[] args) throws InterruptedException {

// warm up the method cache

sumThreadUnsafe();

sumThreadSafe();

sumThreadUnsafe();

sumThreadSafe();

System.out.println(“Starting test”);

long start;

start = System.currentTimeMillis();

sumThreadUnsafe();

long unsafeCost = System.currentTimeMillis() - start;

System.out.println(" thread unsafe: " + unsafeCost + " ms.");

start = System.currentTimeMillis();

sumThreadSafe();

long safeCost = System.currentTimeMillis() - start;

System.out.println(" thread safe: " + safeCost + " ms.");

System.out.println("Thread safety overhead: "

+ ((safeCost * 10000 / (unsafeCost * 100)) - 100) + “%\n”);

}

public static int sumThreadSafe() {

String[] names = new String[] { “Josh”, “James”, “Duke”, “B” };

ThreadSafeObject ts = new ThreadSafeObject();

int sum = 0;

for (int i = 0; i < MAX; i++) {

sum += ts.test(names[i % 4]);

}

return sum;

}

public static int sumThreadUnsafe() {

String[] names = new String[] { “Josh”, “James”, “Duke”, “B” };

ThreadUnsafeObject tus = new ThreadUnsafeObject();

int sum = 0;

for (int i = 0; i < MAX; i++) {

sum += tus.test(names[i % 4]);

}

return sum;

}

}

final class ThreadUnsafeObject {

// private int index = 0;

private int count = 0;

private char[] value = new char[1];

public int test(String str) {

value[0] = str.charAt(0);

count = str.length();

return count;

}

}

final class ThreadSafeObject {

private int index = 0; // remove this line, or just the ‘= 0’ and it will go faster!!!

private int count = 0;

private char[] value = new char[1];

public synchronized int test(String str) {

value[0] = str.charAt(0);

count = str.length();

return count;

}

}

查看英文原文 Do Java 6 threading optimizations actually work? - Part II


志愿参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。

2008 年 10 月 29 日 02:541933
用户头像

发布了 53 篇内容, 共 92425 次阅读, 收获喜欢 2 次。

关注

评论

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

重磅消息,我国数字人民币将在京津冀等具备条件地区试点

CECBC区块链专委会

数字货币 货币

应用研发平台特惠专场,助力企业加速数智化发展

应用研发平台EMAS

实践总结:在 Java 中调用 Go 代码

jiacai2050

守护进程

书旅

php 进程 守护进程

PHP之闭包函数

书旅

面向对象 闭包 函数

终于知道Kafka为什么这么快了!

海星

kafka 消息队列

免费DDoS攻击测试工具大合集

陈磊@Criss

NetPerf揭示容器间是高速路还是林荫小路

陈磊@Criss

腾讯人均月薪7.5w,我这是又被平均了?

程序员生活志

腾讯 职场 薪资

libuv 异步模型之设计概览

Huayra

libuv 异步模型

分苹果

书旅

算法 LeetCode

SpringBoot系列(六):SpringBoot 数据库操作(集成MyBatis)

xcbeyond

Java 微服务 mybatis springboot

英特尔首席架构师Raja:一个“百亿亿次级计算能力惠及每个人”的时代正在到来

最新动态

从实际案例讲 Deno 的应用场景

keelii

Java typescript deno

实用心理学—没用你打我!

代码制造者

职场 职场搞笑 信息技术 人工

JavaScript中的正则表达式详解

华为云开发者社区

Java 正则表达式 程序员 字符串 语法

如何写出完美的接口:接口规范定义、接口管理工具推荐

xcbeyond

Java 架构 接口规范

用Ant实现Java项目的自动构建和部署

陈磊@Criss

Maven的爱恨情仇

xcbeyond

Java maven

如何从红蓝墨水中分离出红墨水?回声消除算法介绍

拍乐云Pano

音视频 WebRTC 回声消除 3A算法 音频技术

统一软件开发过程(RUP)的概念和方法

力软.net/java开发平台

项目管理 软件开发流程

如何正确认识区块链?

CECBC区块链专委会

区块链价值 区块链应用

统一软件开发过程(RUP)的概念和方法

雯雯写代码

Junit执行单元测试用例成功,mvn test却失败的问题和解决方法

陈磊@Criss

Swagger 这一个文章就够了

陈磊@Criss

SpringBoot系列(五):SpringBoot 日志配置(logback)

xcbeyond

Java 微服务 springboot logback

MySQL中timestamp和datetime,你用的对么?

xcbeyond

MySQL 数据库 后端

Facebook开源的数据Mock:Memisis详解

陈磊@Criss

Vue项目起步

JackWangGeek

Vue

合约一键跟单软件开发技术,跟单系统搭建app

WX13823153201

比特币 区块链

分布式链路追踪Skywalking 存储模型设计

胡弦(关注公众号架构治理之道)

分布式 Skywalking 高性能 分布链路追踪

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

Java 6中的线程优化真的有效么?——第二部分-InfoQ