`
willwen
  • 浏览: 26247 次
  • 性别: Icon_minigender_1
  • 来自: 深圳
社区版块
存档分类
最新评论

java内存模型学习

    博客分类:
  • java
 
阅读更多

之前内部培训整理的有关java内存模型的材料,贴出来记录下

什么是Java内存模型

        Java 内存模型 (JMM)描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节。对象最终存储在内存中,但编译器、运行库、处理器或缓存可以有特权定时地在变量的指定内存位置存入或取出变量值。

       例如,编译器为了优化一个循环索引变量,可能会选择把它存储到一个寄存器中,或者缓存会延迟到一个更适合的时间,才把一个新的变量值存入主存。所有的这些优化是为了帮助实现更高的性能,通常这对于用户来说是透明的,但是对多处理系统来说,这些复杂的事情可能有时会完全显现出来。 

为什么需要一个内存模型

       Java 平台把线程和多处理技术集成到了语言中,这种集成程度比以前的大多数编程语言都要强很多。该语言对于平台独立的并发及多线程技术的支持是野心勃勃并且是具有开拓性的,或许并不奇怪,这个问题要比 Java 体系结构设计者的原始构想要稍微困难些。关于同步和线程安全的许多底层混淆是 Java 内存模型的一些难以直觉到的细微差别

原始 JMM 的缺点

      旧的 JMM (jdk5之前)允许一些奇怪而混乱的事情发生,如果您阅读了关于双重检查锁定问题(double-checked locking problem)的任何文章,您将会记得内存操作重新排序是多么的混乱,以及当您没有正确地同步(或者没有积极地试图避免同步)时,细微却严重的问题会如何暗藏在您的代码中。更糟糕的是,许多没有正确同步的程序在某些情况下似乎工作得很好,例如在轻微的负载下、在单处理器系统上,或者在具有比 JMM 所要求的更强的内存模型的处理器上。 

重新排序

“重新排序”这个术语用于描述几种对内存操作的真实明显的重新排序的类型:
1.当编译器不会改变程序的语义时,作为一种优化它可以随意地重新排序某些指令。
2.在某些情况下,可以允许处理器以颠倒的次序执行一些操作。
3.通常允许缓存以与程序写入变量时所不相同的次序把变量存入主存。 
总结:编译器为了进行代码优化,会改变程序的顺序

 重新排序举例

 public class Test{

private int m1;

 

private int m2;

 

//构造函数

 

public Test(){

 

  this.m1 = 1;

 

  this.m2 = 2;

 

}

 

public static void main(){

 

  Test test = new Test();

 

}

 

}

假设在Test对象创建过程中需要初始化两个值域m1和m2,正常的过程应该是:开始对象创建,得到一个对象引用,m1初始化,m2初始化,把这个对象句柄赋值给变量a。 但是由于重排序的存在,可能实际的执行过程变为:开始对象创建,得到一个对象引用,m1初始化,把这个对象引用赋值给变量a,m2初始化。而另一个线程在这个对象引用赋值给变量a后,m2初始化前来访问变量a,并通过a访问到这个创建中的对象,问题出来了,m2初始化还没有完成呢... 所以这个时候就会出现线程安全的问题,需要做出同步的操作

Java线程安全

编写Java多线程程序一直以来都是一件十分困难的事,多线程程序的bug很难测试,DCL(Double Check Lock)就是一个典型,

因此对多线程安全的理论分析就显得十分重要,当然这决不是说对多线程程序的测试就是不必要的。传统上,对多线程程序的分析是通过分析操作之间可能的执行先后顺序,

然而程序执行顺序十分复杂,它与硬件系统架构,编译器,缓存以及虚拟机的实现都有着很大的关系。仅仅为了分析多线程程序就需要了解这么多底层知识确实不值得,

况且当年选择学Java就是因为不用理会烦人的硬件和操作系统,这导致了许多Java程序员不愿也不能从理论上分析多线程程序的正确性

幸运的是,现在有另外一种方法我们只需要利用几个基本的happen-before(JLS 17.4.5)规则就能从理论上分析Java多线程程序的正确性,而且不需要涉及到硬件和编译器的知识。

一个操作happen-before另一个操作

当说操作A happen-before操作B时,我们其实是在说在发生操作B之前,操作A对内存施加的影响能够被观测到。所谓“对内存施加的影响”就是指对变量的写入,

“被观测到”指当读取这个变量时能够得到刚才写入的值。

 

举例:

•线程Ⅰ执行操作A:x=3

•线程Ⅱ执行操作B:y=x。

•如果操作Ahappen-before操作B,线程Ⅱ在执行操作B之前就确定操作"x=3"被执行了,它能够确定,是因为如果这两个操作之间没有任何对x的写入的话,它读取x的值将得到3,

这意味着线程Ⅱ执行操作B会写入y的值为3。

•如果两个操作之间还有对x的写入会怎样呢?假设线程Ⅲ在操作A和B之间执行了操作C: x=5,并且操作C和操作B之前并没有happen-before关系

•这时线程Ⅱ执行操作B会讲到x的什么值呢?(3还是5?)

 

•答案是两者皆有可能,这是因为happen-before关系保证一定 能够观测到前一个操作施加的内存影响,

只有时间上的先后关系而并没有happen-before关系可能但并不保证能观测前一个操作施加的内存影响。

如果读到了值3,我们就说读到了“陈旧 ”的数据。

正是多种可能性导致了多线程的不确定性和复杂性,但是要分析多线程的安全性,我们只能分析确定性部分,这就要求找出happen-before关系,

这又得利用happen-before规则。

Happens-before规则

•下面列出的三条非常重要的happen-before规则,利用它们可以确定两个操作之间是否存在happen-before关系:

1.同一个线程中,书写在前面的操作happen-before书写在后面的操作。这条规则也称为单线程规则

2.对锁的unlock操作happen-before后续的对同一个锁的lock操作。这里的“后续”指的是时间上的先后关系,这里关键条件是必须对“同一个锁”的lock和unlock。

 

3.如果操作A happen-before操作B,操作B happen-before操作C,那么操作A happen-before操作C。这条规则也称为传递规则。 

•看下面的例子:

  public void setX(int x) {

    this.x = x;               // (1)

  }

  public int getX() {

    return x;                 // (2)

  }

假设线程Ⅰ先执行setX方法,接着线程Ⅱ执行getX方法,在时间上线程Ⅰ的操作A:this.x = x先于线程Ⅱ的操作B:return x。

但是操作A却并不happen-before操作B,让我们逐条检查三条happen-before规则。

第1条规则在这里不适用,因为这时两个不同的线程。

第2条规则也不适用,因为这里没有任何同步块,也就没有任何lock和unlock操作。

第3条规则必须基于已经存在的happen-before关系,现在没有得出任何happen-before关系,因此第三条规则对我们也任何帮助。

通过检查这三条规则,我们就可以得出,操作A和操作B之间没有happen-before关系。这意味着如果线程Ⅰ调用了setX(3),接着线程Ⅱ调用了getX(),其返回值可能不是3,尽管两个操作之间没有任何其它操作对x进行写入,它可能返回任何一个曾经存在的值或者默认值0。

 

“任何曾经存在的值”需要做点解释,假设在线程Ⅰ调用setX(3)之前,还有别的线程或者就是线程Ⅰ还调用过setX(5), setX(8),那么x的曾经可能值为0, 5和8(这里假设setX是唯一能够改变x的方法),其中0是整型的默认值,用在这个例子中,线程Ⅱ调用getX()的返回值可能为0, 3, 5和8,至于到底是哪个值是不确定的。

对上例进行改进

•public synchronized void setX(int x) {  

•  this.x = x;               // (1)  

•}  

• 

•public synchronized int getX() {  

•  return x;                 // (2)  

 

•} 

做同样的假设,线程Ⅰ先执行setX方法,接着线程Ⅱ执行getX方法,这时就可以得出来,线程Ⅰ的操作A happen-before线程Ⅱ的操作B。下面我们来看如何根据happen-before规则来得到这个结论。由于操作A处于同步块中,操作A之后必须定要发生对this锁的unlock操作,操作B也处于同步块中,操作B之前必须要发生对this锁的lock操作,根据假设unlock操作发生lock操作之前,根据第2条happen-before规则,就得到unlock操作happen-before于lock操作;另外根据第1条happen-before规则(单线程规则),操作A happen-before于unlock操作,lock操作happen-before于操作B;最后根据第3条happen-before规则(传递规则),A -> unlock, unlock -> lock, lock -> B(这里我用->表示happen-before关系),有 A -> B,也就是说操作A happen-before操作B。这意味着如果线程Ⅰ调用了setX(3),紧接着线程Ⅱ调用了getX(),如果中间再没有其它线程改变x的值,那么其返回值必定是3。

如果将两个方法的任何一个synchronized关键字去掉又会怎样呢?这时能不能得到线程Ⅰ的操作A happen-before线程Ⅱ的操作B呢?答案是得不到。这里因为第二条happen-before规则的条件已经不成立了,这时因为要么只有线程Ⅰ的unlock操作(如果去掉getX的synchronized),要么只有线程Ⅱ的lock操作(如果去掉setX的synchronized关键字)。

即必须对同一个变量的 所有 读写同步,才能保证不读取到陈旧的数据,仅仅同步读或写是不够的 。

 

利用Happen-Before规则分析DCL 

•public class LazySingleton {  

•    private int someField;  

•    private static LazySingleton instance;  

•    private LazySingleton() {  

•        this.someField = new Random().nextInt(200)+1;  // (1)  

•    }  

•    public static LazySingleton getInstance() {  

•        if (instance == null) {                               // (2)   

•            synchronized(LazySingleton.class) {               // (3)  

•                if (instance == null) {                       // (4)   

•                    instance = new LazySingleton();           // (5)   

•                }  

•            }  

•        }  

•        return instance;            // (6)  

•    }  

•    public int getSomeField() {  

•        return this.someField;     // (7)   

•    }  

 

•}  

       线程Ⅰ是初次调用getInstance()方法,紧接着线程Ⅱ也调用了getInstance()方法和getSomeField()方法

整个线程Ⅱ的操作都是在没有同步的情况下调用 ,这时我们无法利用第1条和第2条happen-before规则得到线程Ⅰ的操作和线程Ⅱ的操作之间的任何有效的happen-before关系,这说明线程Ⅰ的语句(1)和线程Ⅱ的语句(7)之间并不存在happen-before关系,这就意味着线程Ⅱ在执行语句(7)完全有可能观测不到线程Ⅰ在语句(1)处对someFiled写入的值,这就是DCL的问题所在!

       前面我们说了,线程Ⅱ在执行语句(2)时也有可能观察空值,如果是种情况,那么它需要进入同步块,并执行语句(4)。在语句(4)处线程Ⅱ还能够读到instance的空值吗?不可能。这里因为这时对instance的写和读都是发生在同一个锁确定的同步块中,这时读到的数据是最新的数据。为也加深印象,我再用happen-before规则分析一遍。线程Ⅱ在语句(3)处会执行一个lock操作,而线程Ⅰ在语句(5)后会执行一个unlock操作,这两个操作都是针对同一个锁--LazySingleton.class,因此根据第2条happen-before规则,线程Ⅰ的unlock操作happen-before线程Ⅱ的lock操作,再利用单线程规则,线程Ⅰ的语句(5) -> 线程Ⅰ的unlock操作,线程Ⅱ的lock操作 -> 线程Ⅱ的语句(4),再根据传递规则,就有线程Ⅰ的语句(5) -> 线程Ⅱ的语句(4),也就是说线程Ⅱ在执行语句(4)时能够观测到线程Ⅰ在语句(5)时对LazySingleton的写入值。接着对返回的instance调用getSomeField()方法时,我们也能得到线程Ⅰ的语句(1) -> 线程Ⅱ的语句(7),这表明这时getSomeField能够得到正确的值。但是仅仅是这种情况的正确性并不妨碍DCL的不正确性,一个程序的正确性必须在所有的情况下的行为都是正确的,而不能有时正确,有时不正确。

改进:

public synchronized int getSomeField() {  

    return this.someField;                             

}  

这样并不正确,这是因为,第2条happen-before规则的前提条件并不成立。语句(5)所在同步块和语句(7)所在同步块并不是使用同一个锁。

应采用下面的改进方式:

•public int getSomeField() {  

•    synchronized(LazySingleton.class) {  

•        return this.someField;  

•    }  

 

•}  

•这样的修改虽然能保证正确性却不能保证高性能

•private static LazySingleton instance;  

•private static int hasInitialized = 0; 

•public static LazySingleton getInstance() {  

•    if (hasInitialized == 0) {                                          // (4)  

•        synchronized(LazySingleton.class) {                         // (5)  

•            if (instance == null) {                                 // (6)  

•                instance = new LazySingleton();                     // (7)  

•                hasInitialized = 1;  

•            }  

•        }  

•    }  

•    return instance;                                                // (8)  

 

•}

如果你明白我前面所讲的,那么很容易看出这里根本就是一个伪修正,线程Ⅱ仍然完全有可能在非同步状态下返回instance。对int变量的赋值是原子的,但实际上对instance的赋值也是原子的,Java语言规范规定对任何引用变量和基本变量的赋值都是原子的,除了long和double以外。使用hasInitialized==0和instance==null来判断LazySingleton有没有初始化没有任何区别。

Effective Java 2nd提示的方法

•public class Singleton {  

•  private Singleton() {}  

•  // Lazy initialization holder class idiom for static fields   

•  private static class InstanceHolder {  

•   private static final Singleton instance = new Singleton();  

•  }  

•  public static Singleton getSingleton() {   

•    return InstanceHolder.instance;   

•  }  

 

•}  

•利用的原理是:一个类直到被使用时才被初始化,而类初始化的过程是非并行的,这些都由JLS保证。

•在java 5中多增加了一条happen-before规则:

 对volatile字段的写操作happen-before后续的对同一个字段的读操作。

•利用这条规则我们可以将instance声明为volatile,即:

 

 private volatile static LazySingleton instance;  

•根据这条规则,我们可以得到,线程Ⅰ的语句(5) -> 语线程Ⅱ的句(2),根据单线程规则,线程Ⅰ的语句(1) -> 线程Ⅰ的语句(5)和语线程Ⅱ的句(2) -> 语线程Ⅱ的句(7),

再根据传递规则就有线程Ⅰ的语句(1) -> 语线程Ⅱ的句(7),这表示线程Ⅱ能够观察到线程Ⅰ在语句(1)时对someFiled的写入值,程序能够得到正确的行为。

 

如果还有不明白的,可以参考下面这篇文章,详细说明了双重检查锁定及单例模式

http://www.ibm.com/developerworks/cn/java/j-dcl.html

 

•结束语:多线程真的很难!!!

 

 

 

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

    Java 内存模型

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

    java内存模型文档

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

    深入理解java内存模型

    深入学习Java内存模型对于编写高效、正确的并发程序至关重要。通过阅读这本书,读者将能够掌握如何在并发环境下正确地管理内存,理解线程间的通信机制,避免并发问题,提高程序的稳定性和性能。

    深入理解 Java 内存模型

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

    java内存模型详解

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它规定了程序中各个线程如何访问共享变量,以及对这些访问进行...深入学习和掌握Java内存模型是每个Java开发者必备的技能之一。

    java内存模型与并发技术

    阿里巴巴专家讲座——java内存模型与并发技术。 主要内容: 学习java并发理论基础:Java Memory Model 学习java并发技术基础:理解同步是如何工作 分析程序什么时候需要同步 几个典型的并发设计策略

    java内存模型

    Java内存模型(Java Memory Model,JMM)是Java虚拟机(JVM)规范中的一个重要组成部分,它定义了程序中各个变量(包括实例域、静态域和数组元素)的访问规则,以及在实际计算机系统中如何将这些变量存储在内存和从...

    深入理解JAVA内存模型(高清完整版)

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它定义了在多线程环境下,如何在共享内存中读写数据,以及如何保证数据的一致性和可见性。本教程《深入理解JAVA内存模型》将带你深入探讨这...

    深入理解Java内存模型(经典).rar

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证共享数据的正确性。深入理解JMM对于编写高效、线程安全的Java代码至关重要。 首先,我们要...

    深入java内存模型

    通过学习《深入Java内存模型》,开发者不仅可以理解Java内存管理的底层机制,还能掌握优化程序性能、避免并发问题的关键技能,从而提升代码质量并降低系统维护成本。这本书是每个Java开发者必备的参考书目,无论你是...

    java内存模型和一些多线程的资料

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它定义了在多线程环境下,如何在共享内存中读写变量的行为。JMM的主要目标是确保多线程环境下的可见性、有序性和原子性,从而避免数据不一致...

    JAVA内存模型.docx

    1. Java内存模型的目的是解决多线程环境下的数据一致性问题。当一个线程修改共享变量时,其他线程必须能够正确地读取这些修改。JMM规定了线程如何看到其他线程对共享变量的修改,并规定了必要的同步策略,如volatile...

    Java内存模型精辟总结

    Java内存模型(JMM)是Java编程中的一个重要概念,它规定了程序中各个变量的访问规则,尤其是在多线程环境下如何保证数据的一致性和可见性。JMM的目标是为了解决由于编译器优化、处理器缓存和多处理器系统间的内存...

    深入理解Java内存模型

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一种抽象概念,它规定了如何在多线程环境下正确地处理共享变量的读写操作,以确保程序的正确性和可见性。深入理解Java内存模型对于进行高效的...

    【深入Java虚拟机】Java内存模型探讨一.pdf

    Java内存模型,简称JVM内存模型,是Java虚拟机(JVM)运行时的核心组成部分,它定义了如何在多线程环境下共享数据以及确保数据一致性。深入理解Java内存模型对于优化程序性能、避免并发问题至关重要。 首先,让我们...

    什么是Java内存模型.docx

    Java内存模型是建立在计算机内存模型基础之上的,旨在解决现代计算机硬件中多处理器架构和高速缓存带来的内存一致性问题。 计算机内存模型主要是为了解决CPU和内存之间的速度差异。早期,CPU和内存速度差距不大,但...

    深入理解Java 虚拟机内存模型.rar

    Java虚拟机(JVM)内存模型是Java编程语言的核心组成部分,它定义了程序运行时的数据区域和内存管理方式。深入理解这一模型对于优化Java应用程序性能、避免内存泄漏以及理解线程安全至关重要。以下是对Java虚拟机...

    深入理解JAVA内存模型。。

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的核心概念,它定义了程序中各个线程如何共享和访问数据,以及在多线程环境下如何...通过阅读"深入理解JAVA内存模型.pdf",可以系统学习和掌握这些关键知识点。

    Java-concurrentMap-内存模型深入分析-HotCode

    本文将深入探讨`concurrentMap`在Java内存模型(JMM,Java Memory Model)中的实现原理,以及如何通过HotCode优化并发性能。 Java内存模型定义了线程之间的共享变量访问规则,确保在多线程环境下正确地同步数据。...

    【Java面试题】Java内存模型

    【Java面试题】Java内存模型

Global site tag (gtag.js) - Google Analytics