`
足至迹留
  • 浏览: 496898 次
  • 性别: Icon_minigender_1
  • 来自: OnePiece
社区版块
存档分类
最新评论

<高级-2> java内存模型

阅读更多
抓住4月尾巴:)

前面介绍的所有原则,比如安全发布,同步策略的规范以及一致性等,他们的安全性都来自于JMM(java内存模型, java memory model)。

1.1 什么是内存模型,为什么需要它
假设一个线程为变量aVariable赋值:
aVariable = 3;
内存模型需要解决这个问题:在什么条件下,读取aVariable的线程将看到这个值为3?在缺少同步的条件下,会有许多因素使得线程无法立即,甚至永远,看到另一个线程的操作结果。
(1)在编译器中生成的指令顺序,可以与源代码的顺序不同,此外编译器还会把变量保存在寄存器而不是内存中;
(2)处理器可以采用乱序或并行等方式来执行指令;
(3)缓存(每个工作线程都有自己的缓存,多处理器时每个处理器也有自己的缓存)可能会改变将写入变量提交到主内存的次序;
(4)而且,保存在处理器本地缓存中的值,对于其他处理器是不可见的。这些因素都会使得一个线程无法看到变量的最新值,并且会导致其他线程中的内存操作似乎在乱序执行(如果没有使用正确的同步)。

JMM规定了JVM必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对于其他线程可见。JMM在设计时就在可预测性和程序的易开发性之间进行了权衡,从而在各种主流的处理器体系架构上能实现高性能的JVM。

1.1.1 平台的内存模型
在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期地与主内存进行协调。在不同的处理器架构中提供了不同级别的缓存一致性(Cache Coherence),其中一部分只提供最小的保证即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值
在架构定义的内存模型中将告诉应用程序可以从内存系统中获得怎样的保证,此外还定义了一些特殊的指令(称为内存栅栏或栅栏),当需要共享数据时,这些指令就能实现额外的存储协调保证。为了使java开发人员无需关心不同架构上内存模型之间的差异,java还提供了自己的内存模型,并且jvm通过在适当的位置上插入内存栅栏来屏蔽在JMM与底层平台内存模型之间的差异。

1.1.2 重排序
下面程序说明了在没有正确同步的情况下,即使要推断最简单的并发程序的行为也很困难。这个程序就可以输出(1,0),(1,1),(0,1),(0,0)。前三种情况很容易想象,


1.1.3 java内存模型简介
java内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁和释放操作,以及线程的启动和合并操作。JMM为程序总所有的操作定义了一个偏序关系,称之为Happens-Before。要想保证执行B操作的线程看到操作A的结果(无论A和B是否在同一个线程中执行),那么在A和B之间必须满足Happens-Before关系。如果两个操作之间缺乏Happens-Before关系,那么JVM可以对它们任意地排序。
当一个变量被多个线程读取并且至少被一个线程写入时,如果在读操作和写操作之间没有依照Happens-Before来排序,那么就会产生数据竞争问题。在正确同步的程序中不存在数据竞争,并且表现出串行一致性,这意味着程序中的所有操作都会按照一种固定的和全局的顺序执行。
Happens-before规则包括:
程序顺序规则:如果程序中操作A在操作B之前,那么在线程中A操作在B操作之前执行。
监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
volatile变量规则:对volatile变量的写入操作必须在对该变量的读操作之前执行。推荐: http://www.cnblogs.com/dolphin0520/p/3920373.html
线程启动规则:在线程上对Thread.start的调用必须在该线程中执行任何操作之前执行。
线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false.
中断规则:当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted)。
终结器规则:对象的构造函数必须在启动该对象的终结器之前执行完成。
传递性:如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A也必须在操作C之前执行。

虽然这些操作只满足偏序关系,但同步操作,如锁的获取与释放等操作,以及volatile变量的读取和写入操作,都满足全序关系


1.2 发布
前面介绍的如何安全地发布一个对象,他们的安全性都来自于JMM提供的保证,而造成不正确发布的真正原因,就是“发布一个共享对象”与“另一个线程访问该对象”之间缺少一种Happens-Before排序。

1.2.1 不安全的发布
缺少Happens-Before关系时,就可能出现重排序问题,这就解释了为什么在没有充分同步的情况下发布一个对象会导致另一个线程看到一个只被部分构造的对象。
(1)在初始化一个新的对象时,需要写入多个变量,即新对象中的各个域。
(2)同样,在发布一个引用时也需要写入一个变量,即新对象的引用。
依据上面两步,如果无法确保发布共享引用的操作在另一个线程加载该共享引用之前执行,那么对新对象引用的写入操作将与对象中各个域的写入操作重排序。在这种情况下,另一个线程可能看到对象引用的最新值,但同时也将看到对象的某些或全部状态中包含的无效值,即一个被部分构造的对象。

错误的延迟初始化将导致不正确的发布。

这是最常见的不安全的延迟初始化方式。

除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用对象的线程开始使用之前执行。

1.2.2 安全初始化模式
有时候我们需要推迟一些高开销的对象初始化操作,并且只有当使用这些对象时才进行初始化,但我们也看到了在误用延迟初始化时导致的问题。
给上面的程序加上同步约束,就成了线程安全的初始化。


在初始器中采用了特殊的方式来处理静态域(或在静态初始化代码块中初始化的值),并提供了额外的线程安全性保证。静态初始化器(也就是static块)是由JVM在类的初始化阶段执行,即在类被加载后并且被线程使用前。由于JVM将在初始化阶段获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。因此无论是在被构造期间还是被引用时,静态初始化的对象都不需要显式的同步。然而,这个规则仅适用于在构造时的状态,如果对象是可变的,那么在读线程和写线程之间仍然需要通过同步来确保随后的修改操作时可见的以及避免数据破坏。
如下还有两种方式可以安全的构造对象。一个是结合静态初始化技术提前初始化对象,一个是使用延迟初始化占位类模式(使用一个专门的类来初始化对象)。



JVM将推迟ResourceHolder的初始化操作,直到开始使用这个类时才初始化,并且由于通过一个静态初始化来初始化Resource,因此不需要额外的同步。
看到没,第二种方法既做到了延迟初始化,又不需要使用同步,非常实用!

1.2.3 双重检查锁(DCL, double check lock)
任何一本介绍并发的书都会讨论声名狼藉的双重检查锁。


上面这样写,线程可能看到一个仅被部分初始化构造的Resource。DCL的真正问题在于,当在没有同步的情况下读取一个共享对象时,可能发生的最糟糕事情只是看到一个失效值。然而,实际情况远比这更糟糕,线程可能看到引用的当前值,但对象的状态值却是失效的,这意味着线程可以看到对象处于无效或错误的状态。

在JMM的后续版本(jdk5.0及以后),如果把resource声明为volatile类型,那么DCL就是安全的,并且对性能的影响很小。然而,DCL的这种使用方法已经被广泛废弃了,延迟初始化占位类模式能带来同样的优势,而且更容易理解。推荐: http://www.iteye.com/topic/652440

1.3 初始化过程中的安全性
如果能确保初始化过程的安全性,那么就可以使得被正确构造的不可变对象在没有同步的情况下也能安全地在多个线程之间共享,而不管是如何发布的,甚至通过某种数据竞争来发布。也即是如果前面的例子Resource是不可变的,那么不安全的延迟初始化写法实际上也是安全的。
如果不能确保初始化的安全性,那么当在发布或线程中没有使用同步时,一些本应该为不可变对象的值将会发生改变。安全性架构依赖于String的不可变形,如果缺少了初始化安全性,那么可能会导致一个安全漏洞,从而使恶意代码绕过安全检查。

对于含有final域的对象,初始化安全性可以防止对对象的初始引用被重排序到构造过程之前。当构造函数完成时,构造函数对final的所有写入操作,以及对通过这些域可以到达的任何变量的写入操作都将被冻结。并且任何获得该对象引用的线程都至少能确保看到被冻结的值。对于通过final域可到达的初始变量的写入操作,将不会与构造过程后的操作仪器被重排序。

初始化安全性意味着,下面程序的SafeStates可以安全地发布,即使通过不安全的延迟初始化,或者在没有同步的情况下将SafeStates的引用放到一个公有的静态域,或者没有使用同步以及依赖于非线程安全的HashSet.


然而,许多对SafeStates的细微修改都可能破坏它的线程安全性。如果states不是final类型,或者存在除构造函数以外的其他方法能修改states,那么初始化安全性将无法确保。如果在SafeStates中还有其他的非final域,那么其他线程仍然可能看到这些域上不正确的值。这也导致了对象在构造函数中逸出。

初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。
  • 大小: 98.4 KB
  • 大小: 74 KB
  • 大小: 47.5 KB
  • 大小: 38 KB
  • 大小: 50.7 KB
  • 大小: 70 KB
  • 大小: 74.1 KB
0
0
分享到:
评论

相关推荐

    spring security 参考手册中文版

    28.2.2 Java EE容器认证 220 29. LDAP认证 220 29.1概述 220 29.2在Spring Security中使用LDAP 221 29.3配置LDAP服务器 221 29.3.1使用嵌入式测试服务器 222 29.3.2使用绑定认证 222 29.3.3加载权限 223 29.4实现类 ...

    <<java nio>> javaNIO的使用

    Java NIO,全称为New Input/Output,是Java在1.4版本引入的一套全新的I/O API,旨在提供一种更高效、更具控制力的I/O操作方式,替代传统的IO流模型。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器...

    Jersey+Jetty框架简单搭建(包含文件上传下载)

    &lt;artifactId&gt;jersey-container-servlet-core&lt;/artifactId&gt; &lt;version&gt;YOUR_JERSEY_VERSION&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.eclipse.jetty&lt;/groupId&gt; &lt;artifactId&gt;jetty-server&lt;/artifactId...

    自己大二时写的SSM框架总结

    - **数据持久化概念**:数据持久化是指将内存中的数据模型转换成持久存储模型(如文件系统或数据库)的过程。MyBatis通过封装JDBC操作,实现了这一过程的自动化,极大地减少了手动设置参数和处理结果集的工作量。 #...

    servlet-2013-08-14

    虽然Servlet仍然是构建Web应用的基础,但随着Java EE(现在称为Jakarta EE)的发展,出现了更高级的组件模型,如Filter和Servlet 3.0引入的Asynchronous Servlet。Spring MVC和JSF等框架也提供了更抽象的编程模型,...

    spark-core3.1.0基础

    &lt;artifactId&gt;scala-library&lt;/artifactId&gt; &lt;version&gt;2.12.12&lt;/version&gt; &lt;/dependency&gt; &lt;!-- Spark 依赖库 --&gt; &lt;dependency&gt; &lt;groupId&gt;org.apache.spark&lt;/groupId&gt; &lt;artifactId&gt;spark-sql_2.12&lt;/artifactId&gt; ...

    java线程-Java内存模型

    2. 原子性问题:Java内存模型并不保证所有的操作都是原子性的。例如,`count += 1`这样的操作实际上包含读、加、写三个步骤,在多线程环境中,如果在这些步骤之间发生线程切换,可能会导致数据不一致。Java提供了...

    自己动手写一个java版简单云相册

    - **设计模式**:项目可能采用了MVC(模型-视图-控制器)设计模式,将业务逻辑、数据处理和用户界面分离,提高代码的可维护性和可扩展性。 - **Dom4j和XPath**:用于解析XML文档,例如在配置文件web.xml中解析...

    深度剖析java内存模型

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

    JSP高级编程

    高级JSP技术&lt;br&gt;第8章 JSP开发平台的搭建:J2EE &lt;br&gt;8.1 J2SDKEE的安装和使用 &lt;br&gt;8.1.1 软硬件的支持 &lt;br&gt;8.1.2 安装 &lt;br&gt;8.2 J2SDKEE的配置 &lt;br&gt;8.2.1 JDBC的配置 &lt;br&gt;8.2.2 事务处理 &lt;br&gt;8.2.3 服务的端口号 &lt;br&gt;...

    <深入Java虚拟机>

    3. **内存模型**:Java内存分为堆内存和栈内存,其中堆用于存储对象实例,栈则存储方法调用。书中会深入探讨Java内存区域,包括堆、栈、方法区、本地方法栈等,以及它们之间的交互。 4. **垃圾收集**:Java的自动...

    spring mvc jpa

    Spring MVC 是Spring框架的一部分,是一个轻量级的、模型-视图-控制器(MVC)架构,用于构建Web应用程序。JPA(Java Persistence API)是Java平台上的一个标准,用于管理关系数据库中的数据,它简化了ORM(对象关系...

    Java内存模型分析与其在编程中的应用.pdf

    Java内存模型规定了对内存和并发操作的高级抽象,其目的是为了提供给开发者一个一致的内存访问语义,隐藏各种平台下的内存访问细节。这是通过一系列规则来实现的,比如happens-before规则,它定义了一种偏序关系来...

    hibernate大学教程

    - **Java对象持久化**: 持久化是指将内存中的数据转换为可以长期存储的状态的过程。在Java中,这通常意味着将对象的状态保存到磁盘上或数据库中,以便可以在未来某个时刻恢复该状态。 #### 二、Hibernate入门与OR...

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

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它...通过阅读《深入理解JAVA内存模型.pdf》和观看38套java高级架构视频教程,你将能更好地理解和应用这些知识,提升你的Java多线程编程能力。

    java面试——深圳-OPPO-Java高级.zip

    - 内存管理:理解Java内存模型,包括堆、栈、方法区和本地方法栈,以及垃圾回收机制。 2. **并发编程**: - 线程:线程的创建、启动、同步和协作,如synchronized、wait/notify、Lock接口等。 - 并发工具类:...

    开始冬眠_Hibernate教程

    &lt;property name="hibernate.connection.url"&gt;jdbc:mysql://localhost:3306/testdb&lt;/property&gt; &lt;property name="hibernate.connection.username"&gt;root&lt;/property&gt; &lt;property name="hibernate.connection.password...

Global site tag (gtag.js) - Google Analytics