Java并发读书笔记(二)
2017-05-03 14:09
190 查看
第三章 Java内存模型
一、基础
1、并发的两个关键问题
线程间通信和线程间同步线程通信机制:
共享内存:隐式通信,显式同步 消息传递:显式通信,隐式同步
Java的并发采用的是共享内存模型。
2、java内存结构
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在,涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
线程A与线程B之间要通信必须经过以下两个步骤:
1、线程A把本地内存A中更新过的共享变量刷新到主内存中去 2、线程B到主内存中去读取A之前已更新过的共享变量
JMM通过控制主内存与每个线程的本地内存之间的交互来为java程序员提供内存可见性保证
3、指令重排序
源码——–》编译器优化重排序——-》指令级并行重排序———-》内存系统重排序———》最终的指令序列后面两个属于处理器的重排序
对于编译器,JMM的编译器会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则
会要求Java编译器在生成指令序列时,插入特定的内存屏障指令,从而禁止特定类型的处理器重排序。
4、并发模型分类
四种内存屏障:LoadLoad:确保load1的数据先于laod2及后续所有load指令进行装载 StoreStore:确保store1的数据对其他处理器的可见性先于store2及后续所有存储指令 LoadStore:确保load装载先于store的存储刷新到内存 StoreLoad:该屏障前的指令全部完成之后才会执行后面的指令(开销大)
5、先行发生(happens-before)
JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。二、重排序
数据依赖性在单线程程序中,对存在控制依赖的操作重排序不会改变执行结果;但在多线程程序中,对存在控制
依赖的操作重排序,可能会改变程序的执行结果。
详见30页的例子
三、顺序一致性
四、volatile内存语义
volatile变量特性:可见性:对一个volatile变量的读,总是能看到(任意线程)对这个变量最后的写入 原子性:对任意单个volatile变量的读、写具有原子性(包括long、double),但类似volatile++ 这种复合操作不具有原子性。
volatile写-读的内存语义:
写:当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存
读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来从主内存中读取共享变量
内存语义的实现:
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
JMM内存屏障插入策略:
在每个volatile写操作前面插入StoreStore屏障 在每个volatile写操作后面插入StoreLoad屏障 在每个volatile读操作后面插入一个LoadLoad、一个LoadStore
五、锁的内存语义
锁的释放和获取的内存语义(和volatile一样)线程释放锁时,会把本地内存中的共享变量刷新到主内存中(对应volatile写)
线程获取锁时,会将线程对应的本地内存置为无效,从而临界区代码必须从主内存读取共享变量(对应volatile读)
锁内存语义的实现:分析ReentrantLock源码
公平锁和非公平锁语义总结:
公平锁和非公平锁释放时,最后都要写一个volatile变量state 公平锁获取时,首先会去读volatile变量 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义
可以看出:锁释放-获取的内存语义的实现至少有下面两种方式
1、利用volatile变量的写-读所具有的内存语义 2、利用CAS所附带的volatile读和volatile写的内存语义
CAS是如何同时具有volatile读和volatile写的内存语义的?
多处理器环境,会为cmpxchg指令加上lock前缀,单处理器不用加(单处理器会维护自身的顺序一致性)
Lock前缀:
1、确保对内存的读-改-写操作原子执行,使用缓存锁定来保证 2、禁止该指令与之前和之后的读和写指令重排序 3、把写缓冲区的所有数据刷新到内存中
上面2、3两点具有的内存屏障效果,足以同时实现volatile读和volatile写的内存语义
concurrent包的通用实现模式
首先,声明共享变量为volatile 然后,使用CAS的原子条件更新来实现线程之间的同步 同时,配合以volatile的读、写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信
六、final域的内存语义
1、final域的重排序规则
(1)写:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作不能重排序(2)读:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作不能重排序。
2、写final域的重排序规则
禁止把final域的写重排序到构造函数之外包含两方面:1、编译器: JMM禁止编译器把final域的写重排序到构造函数之外 2、处理器: 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域 的写重排序到构造函数之外
上述规则可以确保:
在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
3、读final域的重排序规则
处理器:在一个线程中,初次读对象引用与初次读该对象所包含的final域,JMM禁止处理器重排序这两个操作编译器:编译器会在读final域操作的前面插入一个LoadLoad屏障
上述重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用
4、当final域为引用类型
对于引用类型,写final域的重排序规则增加下面的约束:在构造函数内对一个final引用对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量, 这两个操作不能重排序。
5、为什么final域不能从构造函数内溢出
在构造函数返回前,被构造对象的引用不能为其他线程所见,因为此时的final域可能还没有初始化。七、happens-before
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序执行的。happens-before关系给编写正确同步的多线程程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序执行的。
这么做的目的:为了在不改变程序的执行结果的前提下,尽可能地提高程序执行的并行度。
八、双重检查锁定与延迟初始化
在单例的懒汉模式中,必须给实例添加volatile修饰符原因:在构造实例时,对象引用指针的操作和初始化操作可能会被重排序,
这就导致在if(instance==null)的时候认为对象已经创建,但这个时候还没有进行初始化
1.分配对象的内存空间 2.初始化对象 3.设置instance指向内存空间 4.初次访问对象 3和2可能会被重排序,导致1342这样的问题
解决方式:
volatile
基于类初始化的解决方案(还没好好看,记得回头补上)P72
相关文章推荐
- Java并发编程的艺术-读书笔记
- 读书笔记:Java并发实战第15章 原子变量与非阻塞同步机制
- 【Java并发】JAVA并发编程实战-读书笔记18
- Java并发编程实践 读书笔记
- 【Java并发】JAVA并发编程实战-读书笔记15
- 《Thinking in Java》读书笔记之并发(四)
- 《Thinking in Java》读书笔记之并发(五)
- 《Thinking in Java》读书笔记之并发(六)
- synchronized的实现原理-java并发编程的艺术读书笔记
- <Java 并发编程实践>读书笔记 --- 线程封闭
- Java并发编程的艺术-读书笔记
- 【Java并发】JAVA并发编程实战-读书笔记4
- 【Java并发】JAVA并发编程实战-读书笔记19
- Java并发读书笔记(五)
- 【Java并发】JAVA并发编程实战-读书笔记13
- java并发-读书笔记
- Java并发编程艺术----读书笔记(二)
- 《java并发编程的艺术》读书笔记-第四章java并发的基础
- 【读书笔记】Java并发机制的底层实现原理
- 《并发编程的艺术》读书笔记-第2章java并发机制的底层实现原理