Java并发读书笔记(二)

来源:互联网 发布:青藏铁路灵异事件知乎 编辑:程序博客网 时间:2024/05/29 19:52

第三章 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这样的问题

解决方式:

  1. volatile

  2. 基于类初始化的解决方案(还没好好看,记得回头补上)P72

0 0