西西河

主题:【原创】介绍一下Go语言(1)之前的话 -- zllwy

共:💬92 🌺231
全看分页树展 · 主题 跟帖
家园 【原创】介绍一下Go语言(外篇)JMM

Java Memory Model (JMM)

为了说明线程和共享内存模式的问题,可以来看看Java memory model的演化中体现出来的保证共享内存模式正确的微妙性和困难性。

大家都知道Java中三个关键字final, synchronized和volatile。前两个比较容易理解,final是说被标记的变量是个常量,synchronized是表示一段代码由monitor保护,起到一个互斥的作用。至于volatile,是说被标记的变量不会被cache(register, L1/L2 cache or other buffers),直接写到内存。但由于硬件的复杂性,Java memory model并不是从一开始就把这几个关键字的真正用途实现正确了的。在Java 1.4之前,JMM有很多问题。

首先说一下导致问题的根源。几个问题,一是memory hierarchy,包括register,cache,buffer和RAM,一个变量赋值以后,很可能没有立即写到内存里去。二是硬件的分布性。在多个CPU/core的情况下,各个cpu/core可能有自己的cache,这就有了cache coherency的问题。一个线程给一个变量赋值,不见得另外一个线程就能马上看到。还有就是程序指令本身很可能被compiler重新排序来提供运行性能。由于Java是compile once, run everywhere,它就需要保证实现一个memory model,使得程序在任何系统上都运行正确。由于C/C++没有这样的保证,所以写多线程程序的时候要特别小心,很可能在一个硬件平台上运行正确的程序,到了另外一个硬件平台上就不对了。而这些因素使得实现一个跨平台的memory model很有挑战性。

先来看final。在Java 1.4之前,对final的实现不正确。一个final变量,创建的时候先有一个缺省值,然后才赋给初始值,这之间的间隔在多线程的情况下就会导致问题。看下面的例子:

String s1 = “/usr/tmp”;

String s2 = s1.substring(4)

正确的结果,s2应该是”/tmp”。但在Java 1.4之前,多线程情况下,s2可能是”/usr”。这是因为String的实现,有三个final域,包括长度和起始位置,但因为没有对多线程的保证,当String的构造函数被调用时,另一个线程可能会看到final域的缺省值,然后才看到初始值。JSR 133中的新的JMM,保证了这种情况不会出现。

再来看volatile。Volatile的含义很多程序员都不是很清楚。Volatile是说,被标记的变量的变化,应该立即被所有的观察者看到。但在实际对volatile的使用中,往往会出问题。比如下面的例子:

Map config;

volatile boolean initialized = false;

// in thread A

Config = new HashMap();

… // add something to config

Initialized = true;

// in thread B

while (initialized) {

… // use config

}

这里initialized被用作一个guard,保证我们能使用初始化以后的config变量。实际中呢,thread B有可能看到没有初始化过的config。这是因为volatile只保证initialized这个变量立即被其他的线程看到,但由于程序指令的乱序执行,并不能保证initialized赋值之后, config已经准备好了。在JSR 133中,加入了指令的排序,保证了这种情况不会发生。

最后一个是synchronized。表面上,synchronized意味着被保护的程序在一个 monitor里面运行,也就是说是线程互斥的。但实际上还有更深的含义。简单说,就是还有memory barrier的保护。这保证了在monitor上的线程可以以可预见的方式看到互相之间对内存变量的修改。而没有synchronized的代码就没有这种保证。一个例子就是常见的singleton;

Class someClass {

Private Resource resource = null;

Public Resource getInstance() {

If (resource == null) {

Resource = new Resource();

}

Return resource;

}

}

在多线程的情况下,需要给getInstance()加上synchronized。有人就会想优化一下,因为synchronized是有代价的。这个优化叫double-checked locking (DCL):

Class someClass {

Private Resource resource = null;

Public Resource getInstance() {

If (resource == null) {

Synchronized {

If (resource == null) {

Resource = new Resource();

}

}

}

Return resource;

}

}

改进就是先看一下是不是为空,空的话再做synchronized的操作。但DCL是不对的,因为第一个检查没有synchronized保护,很有可能另外一个线程在创建resource的时候,本线程就看到了,然后在resource的constructor被调用完之前就把resource返回了。这就有可能导致对未初始化的object的使用。DCL在新的JMM下也不行。

综合以上的例子,可以看到多线程共享内存的复杂性。写程序的时候一定要知其所以然,加上小心,才能尽量避免多线程编程的错误。

通宝推:铁手,西电鲁丁,
全看分页树展 · 主题 跟帖


有趣有益,互惠互利;开阔视野,博采众长。
虚拟的网络,真实的人。天南地北客,相逢皆朋友

Copyright © cchere 西西河