Java内存模型

规定了线程的工作内存和主内存的交互关系,以及线程之间的可见性和程序的执行顺序

在Java中,采用共享内存模型来实现多线程之间的信息交换和数据同步,线程之间通过共享程序公共的状态,
读写内存中公共状态的方式来进行隐式通信,因此存在内存可见性的问题

并发编程的一些概念

原子性

一个操作或者多个操作要么全部执行要么全部不执行   //通过锁或者Synchronized来解决

可见性

当多个线程同时访问一个共享变量时,如果其中某个线程更改了该共享变量,其他线程应该可以立刻看到这个改变

有序性

程序的执行要按照代码的先后顺序执行

happens-before原则

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性

在JMM中,若操作A的执行结果需要对操作B可见,那么A与B之间须存在happens-before关系

1
2
3
1.如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
2.两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行,如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法

常见关系

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
    1
    2
    1.如果两个操作不存在上述任一一个happens-before规则,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序
    2.如果操作A happens-before操作B,那么操作A在内存上所做的操作对操作B都是可见的

Memory Barrier 内存屏障

Memory Barrier为一个CPU指令,可保证特定操作的执行顺序,影响某些数据的内存可见性(或影响某条指令的执行结果)

1
2
3
4
1.插入一条Memory Barrier指令,可使编译器和CPU不管什么指令都不能和这条Memory Barrier指令重排序
//通常情况下,编译器和CPU尝试优化性,进行重排序指令(但须保证最终结果相同)
2.Memory Barrier指令可强制刷出各种CPU cache
//如一个 Write-Barrier将刷出所有在 Barrier 之前写入 cache 的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本

volatile

volatile就是基于Memory Barrier实现的,如果一个变量是volatile修饰的,JMM会在写入这个字段之后
插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令,
所以,如果写入一个volatile变量a,可以保证:
1、一个线程写入变量a后,任何线程访问该变量都会拿到最新值
2、在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的

指令重排序

重排序要求:在单线程环境下不能改变程序运行的结果、存在数据依赖关系的不允许重排序

重排序是引起多线程不安全的一个重要因素

常见指令重排序

1、编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
2、指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
3、内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性
编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序

as-if-serial语义

所有的操作均可以为了优化而被重排序,但执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义
仅支持单线程环境

重排序不会影响单线程环境的执行结果,但是会破坏多线程的执行语义

总结

java线程之间的通信由JMM控制,JMM决定一个线程对共享变量(实例域、静态域和数组)的写入何时对其它线程可见

线程A和线程B通信步骤如下
1、线程A把本地内存中更新过的共享变量刷新到主内存中
2、线程B到主内存中读取线程A之前更新过的共享变量