什么是可见性
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到。
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
并发编程的两个关键问题
- 线程之间如何通信(以何种机制来交换信息)
- 线程之间如何同步
线程之间的通信机制有两种
- 共享内存,线程共享程序的公共状态,通过写-读公共状态达到隐式通信 。
- 消息传递,线程之间没有公共状态,线程之间必须通过发送消息来显示进行通信。java中典型的消息传递方式就是wait()和notify()。
同步
指的是程序中用于控制不同线程间操作发生相对顺序的机制。
- 在共享内存并发模型中,同步是显示进行的,因为程序员必须显示的指定某个方法或者某段代码在线程之间是互斥执行的。
- 在消息传递并发模型中,同步是隐式进行的,因为消息发送必须在消息接受之前。
JAVA的并发采用的是共享内存模型,线程之间的通信总是隐式的,通信过程对于程序员完全透明。
内存模型的抽象结构
java中素所有实例域、静态域和数组元素都存储在堆内存中,堆内存是线程共享的。局部变量、方法定义参数、异常处理器参数不会再线程之间共享,不会有内存可见性问题。
Java线程间通信由Java内存模型(JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。
JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存中,每个线程都有一个本地内存,本地内存中存储了共享变量的副本,本地内存是JMM的一个抽象概念并不真实存在。 Java的并发采用的是共享内存模型
线程AB之间通信 就是 线程A把本地内存A中的更新后的共享变量刷新到主内存中 然后线程B到主内存中去读取线程A之前已更新过的共享变量,实质上是线程A向线程B发送消息。
JMM通过控制主内存和每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
重排序分为三种类型
- 编译器优化的重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。 将多条指令重叠执行,如果不存在数据依赖性,处理器可改变语句对应机器指令的执行顺序。
- 内存系统重排序。由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是乱序执行的。
1属于编译器重排序 2,3属于处理器重排序
内存屏障指令
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。内存屏障有四种
load 简称l store简称 Barriers简称b
- llb
- ssb
- lsb
- slb
排列顺序与执行的先后顺序相同
其中slb是比较全能的一个屏障 它确保store1数据对其他处理器变得可见(指刷新到内存)先于load2
及所有后续装载指令的装载 并且会使这个屏障之前所有内存访问之灵完成之后在赤星该屏障之后的内存访问指令
同时具备了其他三个屏障的效果,但是这个屏障开销昂贵,因为处理器会把写缓存中的所有数据全部刷新到内存中去。
数据依赖性
编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序
什么是数据依赖性?
如果两个操作访问同一个变量且这两个操作中有一个为写操作,此时两个操作之间就存在数据依赖性
happens-before
JSR-133内存模型使用这个概念来阐述操作之间的内存可见性,在JMM中如果一个操作的执行结果需要对另一个操作看见,那么这两个操作之间必须存在 happens-before关系
happens-before有两个规则
- 程序顺序规则,一个线程中的每个操作happens-before后续操作
- 监视器锁规则,对一个锁的解锁happens-before这个锁的加锁
- volatile变量规则,对一个volatile域的写 happens-before对这个变量的读
- 传递性 a>b b>c a>c。。
happens-before关系 并不是说 A h-b B A就一定先于B执行 而是要求A的执行结果对B可见。
只要多线程的程序是正确同步的,JMM保证该程序在任意处理器平台上的执行结果与改程序在顺序一致性内存模型中的执行结果一致。
Volatile
是java虚拟机提供的最轻量级的同步机制。它有两层语义
-
保证次变量对所有线程的可见性,这里可见性是指当一条线程修改了这个变量的值,新值对于其它线程来说就是立即可得知的。
由于Volatile变量只能保证可见性 以此以下两种场景,仍然需要加锁来保证原子性
- 运算结果依赖变量的当前值,如(i++),或者不能确保只有单一的线程修改变量的值
- 变量需要与其他的状态变量共同参与不变约束。
禁止指令重排序优化
Java内存模型特征
1. 原子性
由java内存模型来直接保证的原子性变量操作包括read,load,assign,use,store,write.如果场景需要更大范围的原子性保证,java内存模型还提供了lock和unlock操作来满足需求,但是并没有把这两个操作直接开放给用户使用,而是提供了更高层次的字节码指令monitorenter,monitorexit来隐式地使用这两个操作,这两个字节码指令反映到代码中就是同步快 synchronized关键字,在这个块里面的操作具有原子性
2.可见性
一个线程对共享变量值的修改,其它线程能够立即得知这个修改,java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。
volatile变量和普通变量的区别是volatile变量的特性保证了新值能够立即同步到主内存,每次使用立即从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。
除了volatile,synchronized和final也能实现可见性。
同步块之所以获取可见性是因为对一个变量执行unlock操作之前,必须先把此变量同步到主内存中
对于final变量,因为final字段在构造器中一旦初始化完成,并且构造器没有吧this的引用传递出去,那在其他线程中就能够看到final字段的值
3. 有序性
如果在本线程中观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的,前半句是 指线程内表现为串行 的语义,后半句是指指令重排序和工作内存和主内存同步延迟现象。
java提供volatile和synchronized两个关键字来保证线程之间操作的有序性。volatile本身就禁止指令重排序,synchronized又规定一个变量在同一时刻只允许一条线程对其进行lock操作,这就决定了持有同一个锁的两个同步块只能串行地进入