`
zhanshenny
  • 浏览: 264506 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java内存模型[转]

阅读更多

http://www.zlmind.com/?p=668
了解Java的同步秘密之前,先来看看JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型。而且Java语言最大的特点就是废除了指针,把程序员从痛苦中解脱出来,不用再考虑内存使用和管理方面的问题。
可惜世事总不尽如人意,虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。

JMM主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数 据交换/同步等繁琐工作均由虚拟机负责完成。如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主 存完成。



 

图1 Java内存模型示例图

线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和 use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。具体的描述请参见JLS第17章。

我们在前面的章节介绍了synchronized的作用,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
(1) 获取同步对象monitor (lock)
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
(5) 释放同步对象锁 (unlock)
可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。如果没有使用synchronized关键字,JVM不 保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者 更新主内存内容,可以由具体的虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量 的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

二、DCL失效
这一节我们要讨论的是一个让Java丢脸的话题:DCL失效。在开始讨论之前,先介绍一下LazyLoad,这种技巧很常用,就是指一个类包含某个成员变量,在类初始化的时候并不立即为该变量初始化一个实例,而是等到真正要使用到该变量的时候才初始化之。
例如下面的代码:
代码1

class Foo {
private Resource res = null ;

public Resource getResource() {
if (res == null )
res = new Resource();
return res;
}
}

由于LazyLoad可以有效的减少系统资源消耗,提高程序整体的性能,所以被广泛的使用,连Java的缺省类加载器也采用这种方法来加载Java类。
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的Race Condition,得用synchronized关键字把上面的方法同步起来。代码如下:
代码2

Class Foo {
Private Resource res = null ;
Public synchronized Resource getResource() {
If (res == null )
res = new Resource();
return res;
}
}

现在Race Condition解决了,一切都很好。

N天过后,好学的你偶然看了一本Refactoring的魔书,深深为之打动,准备自己尝试这重构一些以前写过的程序,于是找到了上面这段代码。你已经不 再是以前的Java菜鸟,深知synchronized过的方法在速度上要比未同步的方法慢上100倍,同时你也发现,只有第一次调用该方法的时候才需要 同步,而一旦res初始化完成,同步完全没必要。所以你很快就把代码重构成了下面的样子:
代码3

Class Foo {
Private Resource res = null ;
Public Resource getResource() {
If (res == null ){
synchronized ( this ){
if (res == null ){
res = new Resource();
}
}
}
return res;
}
}

这种看起来很完美的优化技巧就是Double-Checked Locking。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。

造成DCL失效的原因之一是编译器的优化会调整代码的次序。只要是在单个线程情况下执行结果是正确的,就可以认为编译器这样的“自作主张的调整代码次序” 的行为是合法的。JLS在某些方面的规定比较自由,就是为了让JVM有更多余地进行代码优化以提高执行效率。而现在的CPU大多使用超流水线技术来加快代 码执行速度,针对这样的CPU,编译器采取的代码优化的方法之一就是在调整某些代码的次序,尽可能保证在程序执行的时候不要让CPU的指令流水线断流,从 而提高程序的执行速度。正是这样的代码调整会导致DCL的失效。为了进一步证明这个问题,引用一下《DCL Broken Declaration》文章中的例子:
设一行Java代码:

Objects[i].reference = new Object ();

经过Symantec JIT编译器编译过以后,最终会变成如下汇编码在机器中执行:
0206106A mov eax,0F97E78h
0206106F call 01F6B210 ;为Object申请内存空间
; 返回值放在eax中
02061074 mov dword ptr [ebp],eax ; EBP 中是objects[i].reference的地址
; 将返回的空间地址放入其中
; 此时Object尚未初始化
02061077 mov ecx,dword ptr [eax] ; dereference eax所指向的内容
; 获得新创建对象的起始地址
02061079 mov dword ptr [ecx],100h ; 下面4行是内联的构造函数
0206107F mov dword ptr [ecx+4],200h
02061086 mov dword ptr [ecx+8],400h
0206108D mov dword ptr [ecx+0Ch],0F84030h

可见,Object构造函数尚未调用,但是已经能够通过objects[i].reference获得Object对象实例的引用。
如果把代码放到多线程环境下运行,某线程在执行到该行代码的时候JVM或者操作系统进行了一次线程切换,其他线程显然会发现msg对象已经不为空,导致 Lazy load的判断语句if(objects[i].reference == null)不成立。线程认为对象已经建立成功,随之可能会使用对象的成员变量或者调用该对象实例的方法,最终导致不可预测的错误。

原因之二是在共享内存的SMP机上,每个CPU有自己的Cache和寄存器,共享同一个系统内存。所以CPU可能会动态调整指令的执行次序,以更好的进行 并行运算并且把运算结果与主内存同步。这样的代码次序调整也可能导致DCL失效。回想一下前面对Java内存模型的介绍,我们这里可以把Main Memory看作系统的物理内存,把Thread Working Memory认为是CPU内部的Cache和寄存器,没有synchronized的保护,Cache和寄存器的内容就不会及时和主内存的内容同步,从而 导致一条线程无法看到另一条线程对一些变量的改动。
结合代码3来举例说明,假设Resource类的实现如下:

Class Resource{
Object obj;
}

即Resource类有一个obj成员变量引用了Object的一个实例。假设2条线程在运行,其状态用如下简化图表示:



图2
现在Thread-1构造了Resource实例,初始化过程中改动了obj的一些内容。退出同步代码段后,因为采取了同步机制,Thread-1所做的 改动都会反映到主存中。接下来Thread-2获得了新的Resource实例变量res,由于没有使用synchronized保护所以Thread- 2不会进行刷新工作内存的操作。假如之前Thread-2的工作内存中已经有了obj实例的一份拷贝,那么Thread-2在对obj执行use操作的时 候就不会去执行load操作,这样一来就无法看到Thread-1对obj的改变,这显然会导致错误的运算结果。此外,Thread-1在退出同步代码段 的时刻对ref和obj执行的写入主存的操作次序也是不确定的,所以即使Thread-2对obj执行了load操作,也有可能只读到obj的初试状态的 数据。(注:这里的load/use均指JMM定义的操作)

有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。在William Pugh的论文《Fixing the java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。

如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。代码如下:

Class ResSingleton {
public static Resource res = new Resource();
}

这里ResSingleton只有一个静态成员变量。当第一次使用ResSingleton.res的时候,JVM才会初始化一个Resource实例,并且JVM会保证初始化的结果及时写入主存,能让其他线程看到,这样就成功的实现了LazyLoad。
除了这个办法以外,还可以使用ThreadLocal来实现DCL的方法,但是由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失,有兴趣的读者可以参考文后的参考资料。

最后要说明的是,对于DCL是否有效,个人认为更多的是一种带有学究气的推断和讨论。而从纯理论的角度来看,存取任何可能共享的变量(对象引用)都需要同 步保护,否则都有可能出错,但是处处用synchronized又会增加死锁的发生几率,苦命的程序员怎么来解决这个矛盾呢?事实上,在很多Java开源 项目(比如Ofbiz/Jive等)的代码中都能找到使用DCL的证据,我在具体的实践中也没有碰到过因DCL而发生的程序异常。个人的偏好是:不妨先大 胆使用DCL,等出现问题再用synchronized逐步排除之。也许有人偏于保守,认为稳定压倒一切,那就不妨先用synchronized同步起 来,我想这是一个见仁见智的问题,而且得针对具体的项目具体分析后才能决定。还有一个办法就是写一个测试案例来测试一下系统是否存在DCL现象,附带的光 盘中提供了这样一个例子,感兴趣的读者可以自行编译测试。不管结果怎样,这样的讨论有助于我们更好的认识JMM,养成用多线程的思路去分析问题的习惯,提 高我们的程序设计能力。

三、Java线程同步增强包
相信你已经了解了Java用于同步的3板斧:synchronized/wait /notify,它们的确简单而有效。但是在某些情况下,我们需要更加复杂的同步工具。有些简单的同步工具类,诸如 ThreadBarrier,Semaphore,ReadWriteLock等,可以自己编程实现。现在要介绍的是牛人Doug Lea的Concurrent包。这个包专门为实现Java高级并行程序所开发,可以满足我们绝大部分的要求。更令人兴奋的是,这个包公开源代码,可自由 下载。且在JDK1.5中该包将作为SDK一部分提供给Java开发人员。

Concurrent Package提供了一系列基本的操作接口,包括sync,channel,executor,barrier,callable等。这里将对前三种接口及其部分派生类进行简单的介绍。

sync接口: 专门负责同步操作,用于替代Java提供的synchronized关键字,以实现更加灵活的代码同步。其类关系图如下:



图3 Concurrent包Sync接口类关系图
Semaphore:和前面介绍的代码类似,可用于pool类实现资源管理限制。提供了acquire()方法允许在设定时间内尝试锁定信号量,若超时则返回false。

Mutex:和Java的synchronized类似,与之不同的是,synchronized的同步段只能限制在一个方法内,而Mutex对象可以作为参数在方法间传递,所以可以把同步代码范围扩大到跨方法甚至跨对象。

NullSync:一个比较奇怪的东西,其方法的内部实现都是空的,可能是作者认为如果你在实际中发现某段代码根本可以不用同步,但是又不想过多改动这段 代码,那么就可以用NullSync来替代原来的Sync实例。此外,由于NullSync的方法都是synchronized,所以还是保留了“内存壁 垒”的特性。

ObservableSync:把sync和observer模式结合起来,当sync的方法被调用时,把消息通知给订阅者,可用于同步性能调试。

TimeoutSync:可以认为是一个adaptor,其构造函数如下:
public TimeoutSync(Sync sync, long timeout){…}
具体上锁的代码靠构造函数传入的sync实例来完成,其自身只负责监测上锁操作是否超时,可与SyncSet合用。

Channel接口: 代表一种具备同步控制能力的容器,你可以从中存放/读取对象。不同于JDK中的Collection接口,可以把Channel看作是连接对象构造者(Producer)和对象使用者(Consumer)之间的一根管道。如图所示:



图4 Concurrent包Channel接口示意图

通过和Sync接口配合,Channel提供了阻塞式的对象存取方法(put/take)以及可设置阻塞等待时间的offer/poll方法。实现 Channel接口的类有 LinkedQueue,BoundedLinkedQueue,BoundedBuffer,BoundedPriorityQueue,SynchronousChannel,Slot 等。



图5 Concurrent包Channel接口部分类关系图

使用Channel我们可以很容易的编写具备消息队列功能的代码,示例如下:
代码4

Package org.javaresearch.j2seimproved.thread;

Import EDU.oswego.cs.dl.util.concurrent.*;

public class TestChannel {
final Channel msgQ = new LinkedQueue(); //log信息队列

public static void main( String [] args) {
TestChannel tc = new TestChannel();
For( int i = 0;i < 10;i ++){
Try{
tc.serve();
Thread .sleep(1000);
} catch ( InterruptedException ie){
}
}
}

public void serve() throws InterruptedException {
String status = doService();
//把doService()返回状态放入Channel,后台logger线程自动读取之
msgQ.put(status);
}

private String doService() {
// Do service here
return “service completed OK! ” ;
}

public TestChannel() { // start background thread
Runnable logger = new Runnable () {
public void run() {
try {
for (; ; )
System .out.println( “Logger: ” + msgQ.take());
}
catch ( InterruptedException ie) {}
}
};
new Thread (logger).start();
}
}

Excutor/ThreadFactory接口: 把相关的线程创建/回收/维护/调度等工作封装起来,而让调用者只专心于具体任务的编码工作(即实现Runnable接口),不必显式创建Thread类实例就能异步执行任务。
使用Executor还有一个好处,就是实现线程的“轻量级”使用。前面章节曾提到,即使我们实现了Runnable接口,要真正的创建线程,还是得通过 new Thread()来完成,在这种情况下,Runnable对象(任务)和Thread对象(线程)是1对1的关系。如果任务多而简单,完全可以给每条线程 配备一个任务队列,让Runnable对象(任务)和Executor对象变成n:1的关系。使用了Executor,我们可以把上面两种线程策略都封装 到具体的Executor实现中,方便代码的实现和维护。
具体的实现有: PooledExecutor,ThreadedExecutor,QueuedExecutor,FJTaskRunnerGroup等
类关系图如下:



图6 Concurrent包Executor/ThreadFactory接口部分类关系图
下面给出一段代码,使用PooledExecutor实现一个简单的多线程服务器
代码5

package org.javaresearch.j2seimproved.thread;
import java.net.*;
import EDU.oswego.cs.dl.util.concurrent.*;

public class TestExecutor {
public static void main( String [] args) {
PooledExecutor pool =
new PooledExecutor( new BoundedBuffer(10), 20);
pool.createThreads(4);
try {
ServerSocket socket = new ServerSocket (9999);
for (; ; ) {
final Socket connection = socket.accept();
pool.execute( new Runnable () {
public void run() {
new Handler ().process(connection);
}
});
}
}
catch ( Exception e) {} // die
}
static class Handler {
void process( Socket s){
}
}
}

 

  • 大小: 12.1 KB
分享到:
评论

相关推荐

    Java 内存模型

    Java内存模型是Java虚拟机规范中定义的一部分,它规定了Java程序中变量的读写行为,以及线程之间的交互规则。理解Java内存模型对于编写正确、高效的多线程程序至关重要。在Java 5之前,Java内存模型的描述比较模糊,...

    java 内存模型 java 内存模型

    java 内存模型 java 内存模型 java 内存模型 java 内存模型

    深入理解Java内存模型 pdf 超清版

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了程序中各个线程如何访问和修改共享变量,以及如何确保数据的一致性。深入理解Java内存模型对于编写高效的并发程序至关重要。本文...

    深入理解Java内存模型

    Java内存模型是并发编程中一个至关重要的概念,它定义了共享变量的访问规则,以及这些变量如何在多线程环境下进行读写操作。在深入理解Java内存模型之前,我们需要先了解并发编程模型的分类,然后掌握Java内存模型的...

    cpu 内存模型和java内存模型

    Java程序员了解CPU以及相关的内存模型,对于深入理解Java内存模型以及并发编程至关重要。CPU作为计算机硬件的核心,其架构和工作原理影响着软件的性能和执行效率。尤其在Java这种多线程、高并发的编程语言中,对CPU...

    Java内存模型的历史变迁

    ### Java内存模型的历史变迁 #### 一、引言 随着多核处理器的普及与高性能计算需求的增长,Java作为主流编程语言之一,对于并发处理的支持变得越来越重要。Java内存模型(Java Memory Model,简称JMM)作为Java...

    java内存模型文档

    这些文档如"Java内存模型.docx"、"Java内存模型2.docx"、"深入Java核心 Java内存分配原理精讲.docx"、"java内存模型.pdf"将深入探讨这些概念,帮助开发者更深入地理解Java内存模型及其在实际编程中的应用。...

    深度剖析java内存模型

    Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)规范中定义的一种内存模型,它涉及了线程之间共享变量的可见性问题。在并发编程中,理解Java内存模型对于编写正确的多线程程序至关重要。 首先,...

    深入理解 Java 内存模型_程晓明_InfoQ_java_内存模型_

    Java内存模型,简称JMM(Java Memory Model),是Java编程语言规范的一部分,它定义了线程如何共享和访问内存,以及在多线程环境中如何保证数据一致性。理解JMM对于编写高效、正确且线程安全的Java代码至关重要。 ...

    Java内存模型详解

    ### Java内存模型详解 #### 1. JMM简介 ##### i. 内存模型概述 Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)的一部分,用于规定程序中的各种变量(包括实例字段、静态字段和数组元素等)在多个...

    Java内存模型详解JMM.docx

    Java内存模型详解JMM Java内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)中的一种内存模型,它描述了程序中各个变量之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节...

    深入理解java内存模型

    Java内存模型(Java Memory Model,JMM)是Java平台中非常关键的概念,它定义了线程如何共享和访问内存中的数据,以及在多线程环境下如何保证数据的一致性。这本书"深入理解Java内存模型"显然是为了帮助读者深入探讨...

    java内存模型.pdf

    Java内存模型(JMM)是Java虚拟机(JVM)的一部分,它定义了程序中不同变量如何交互,特别是在多线程环境下。JMM确保了在各种硬件和操作系统平台上,Java程序的行为具有一致性和可预测性。Java内存模型的主要目标是...

    深入理解 Java 内存模型

    Java 内存模型(Java Memory Model,简称 JMM)是 Java 平台中关于线程如何访问共享变量的一套规则,它定义了线程之间的内存可见性、数据一致性以及指令重排序等关键概念,对于多线程编程和并发性能优化至关重要。...

    java内存模型详解--非常经典

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它定义了程序中各个变量的访问规则,以及在多线程环境下的内存一致性效果。JMM主要解决的是并发环境下不同线程之间如何共享数据以及如何保证...

    《深入理解JAVA内存模型》PDF

    Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main ...

    JAVA内存模型与垃圾回收

    JAVA内存模型与垃圾回收是Java开发中至关重要的概念,它们直接影响到程序的性能和稳定性。首先,我们来看看Java内存模型。 Java内存模型,通常被称为JVM内存模型,它定义了程序中不同部分如何访问和共享数据。在...

    java内存模型详解

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它规定了程序中各个线程如何访问共享变量,以及对这些访问进行同步控制的规则。理解Java内存模型对于编写多线程并发程序至关...

Global site tag (gtag.js) - Google Analytics