java并发编程的艺术读书笔记--Java内存模型(2)

volatile的内存语义

3.4.1 volatile的特性

一个volatile变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。volatile变量自身具有下列特性。

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

  • 原子性。对任意当volatile变量的读/写具有原子性,但类似与volatile这种符合操作不具有原子性。

3.4.2 volatile 写-读建立的happens-before关系

根据程序顺序规则,volatile读写规则

3.4.3 volatile写-读的内存含义

  • volatile写内存的含义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • volatile读内存的含义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

总结:

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

3.4.4 volatile内存语义的实现

3.5 锁的内存语义

3.5.1 锁的释放-获取 建立的happens-before关系

3.5.2 锁的释放和获取的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到内存中。 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。

3.5.2 锁内存语义的实现

3.5.4 concurrent包的实现

concurrent 的通用化实现模式

  • 声明共享变量为volatile。
  • 使用CAS的原子条件更新来实现线程之间的同步
  • 配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

Lock、同步器、阻塞队列、Executor、并发容器、AQS、非阻塞数据结构、原子变量类、volatile变量的读/写、CAS。

3.6 final域的内存语义

3.6.1 final域的重排序规则

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能排序。

3.6.2 写final域的重排序规则

写final域的重排序规则禁止把final域的写重排序到构造函数之外。包括以下两个方面:

  • JMM禁止编译器把final域的写重排序到构造函数之外。
  • 编译器会在final域的写之后,构造函数return之前,插入一个storeStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。

3.6.3 读final域的重排序规则

规则是:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。
初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

3.6.4 final域为引用类型

写final域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对应的引用赋值给一个引用变量,这两个操作之间不能重排序。

3.6.5 final引用不能从构造函数内“溢出”的原因

3.6.6 final语义在处理器中的实现

3.7 happens-before

3.7.1 JMM设计

JMM设计要考虑的因素:程序员希望基于一个强内存模型来编写代码,编译器和处理器希望实现一个弱内存模型,便于优化。(矛盾的需求啊!)
JMM把happens-before要求禁止的重排序分为了下面两类:

  • 会改变程序执行结果的重排序。--------采取的策略:JMM要求编译器和处理器必须禁止这种重排序。
  • 不会改变程序执行结果的重排序。------采取的策略:JMM对编译器和处理器不做要求(允许这种重排序)。

happens-before的定义

as-if-serial 与happens-before:as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻镜:单线程程序是按照程序的顺序来执行的。happens-before 关系给编写同步的多线程程序的程序员创造了一个幻镜:正确同步的多线程程序是按happens-before指定的顺序来执行的。

happens-before规则

规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且Bhappens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么线程A的ThreadB.start()操作happens-before于线程B中的任意操作。
  • Join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join() 操作成功返回。

3.8 双重检查锁定与延迟初始化 (结合单例模式的实现)

由来:在推迟一些高开销的对象初始化操作,并且只有在使用这个对象时才进行初始化。这次会采用延迟初始化技术。导致的问题:这种方法是线程不安全的,使用同步机制(synchronized)会导致性能开销。于是:使用双重检查锁定,降低开销,保证线程安全。 ------理想是好的,但是并没有解决线程安全的问题

3.8.2 问题的根源

在 instace = new instance();不是一个原子操作,可以被分解为三步:1.分配对象的内存空间,2.初始化对象,3.设置instace()指向刚分配的内存地址。 其中步骤 2和3之间,可以被重排序。导致对象被分配了地址,确没有初始化。

3.8.3 基于volatile的解决方案

将 instace声明为volatile变量。

3.8.4 基于类初始化的解决方案

JVM在类的初始化阶段(既在calss 被加载后,且被多线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

类初始化的处理过程:

  • 第一阶段:通过在class对象上同步(既获取class对象的初始化锁),来控制类或接口的初始化。这个获取锁的线程会一直等待,直到当前线程能够获取到这个初始化锁。
  • 第二阶段:线程A执行类的初始化,同时线程B在初始化锁对应的condition上等待。
  • 第三阶段:线程A设置state = initialized,然后唤醒在condition中等待的所有线程。
  • 第四阶段:线程B结束类的初始化处理。
  • 第五阶段:线程C执行类的初始化处理。

3.9 java内存模型综述

3.9.1 处理器的内存模型

JMM和处理器内存模型的设计时会以 顺序一致性内存模型为参照。根据对不同类型的 读、写 操作组合的执行顺序的放松,就可以把常见的内存模型分为如下几种:

  • 放松程序中写-读操作的顺序,由此参数了Total Store Ordering 内存模型(TSO)。
  • 在上面的基础上,继续放松程序中 写-写操作的顺序,由此产生了 Partial Store Order 内存模型(PSO)
  • 在前面两条的基础上,继续放松对程序中读-写和读-读操作的顺序,由此产生了Relaxed Memory Order 内存模型(RMO)和PowerPC内存模型。

由于常见的处理器内存模型比JMM要弱,Java编译器在生成字节码是,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台上为java程序员呈现了一个一致的内存模型。

3.9.2 各种内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

3.9.3 JMM的内存可见性保证

  • 单线程程序:单线程程序不会出现内存可见性问题,编译器、runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致型模型中的执行结果相同。

  • 正确同步的多线程程序:正确同步的多线程程序的执行将具有顺序一致性,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

  • 未同步/未正确同步的多线程程序:JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,nul,false);

转载于:https://my.oschina.net/u/3421984/blog/1594841

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值