java内存模型(JMM)

并发程序要比串行程序复杂,一个原因是并发程序下数据访问的一致性和安全性问题对于串行程序来说,第一个程序读取一个变量,变量的值是10,那么程序读到的变量值就是10.但是在并行程序中,读到的变量值就不一定是10,因为并行的程序中如果不加控制任由线程胡乱并行,就可能造成数据错乱的情况

1.原子性

原子性是指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
对于int类型的变量,当两个线程对于int类型的变量赋值的时候,那么不管两个线程以何种方式,怎样工作,int类型的变量的值肯定为两个线程的其中的一个的值
而对于long类型的变量,就不一定呢,对于32位的系统,long的读写不是原子性的(long是64位)

2.可见性

可见性是指一个线程修改了某一个共享变量的值,其他线程是否能够立刻知道这个修改。
对于串行程序,可见性的问题是不存在的,因为是串行的,所以在任何一个操作中修改了某个值,在下一个操作中,读取这个值,就一定是修改后的值
对于并行程序,如果一个线程修改了一个全局变量A,那么其他线程并不一定能马上知道这个改动
如图所示:
java内存模型(JMM)

在cpu1和cpu2各运行一个线程(c1,c2),共享变量t,由于编译器优化或者硬件优化等其他的缘故,c1线程将变量他进行了优化,放到了缓存或者寄存器中。如果c2线程修改了变量t的实际值,而c1线程不知道这个改动,继续读取缓存或寄存器中变量t的值,因此就产生了可见性问题

外在表现:变量t的值被修改,但是c1线程依然只是读取到一个旧的值
造成可见性的问题:

  • 缓存优化
  • 硬件优化
  • 指令重排
  • 编辑器优化

在一个线程中去观察另外一个线程的变量,它们的值是否能观测到,何时能观测到都是没有保证的

3.有序性

对于一个线程的执行代码而言,总是习惯的认为代码的执行时从先往后,一次执行,在没有并发的情况下,这个认知是没有错误的。但是在并发的情况下,程序的执行可能就会出现乱序,写在前面的代码会在后面执行。有序性的问题就是因为程序的执行时,可能进行指令重排,重排后的指令与原来的指令未必一致
例如:
java内存模型(JMM)
假设线程A首先执行writer()方法,接着线程B执行reader()方法,如果出现指令重排,那么线程B不一定能看到a已经赋值为1了

指令重排有一个基本前提,就是保证串行语义的一致性,指令重排不会使串行的语义逻辑发生问题

指令执行步骤

  • 取指 IF (用到PC寄存器和存储器)
  • 译码和取寄存器操作数 ID (用到指令寄存器)
  • 执行或者有效地址机选 EX (用到ALU算术逻辑单元,cpu的执行单元)
  • 存储器访问 MEM
  • 写会 WB (需要寄存器组)
    流水线技术执行指令:
    java内存模型(JMM)
    假如一个步骤需要1毫秒,指令1执行完需要5毫秒,那么指令2需要等到5毫秒才能执行
    当使用流水线执行指令的时候,指令2只需要等待1毫秒,等指令1执行完IF步骤,释放硬件之后,指令2就能调用IF所需要的硬件进行实行步骤,后续的步骤也是同理。如此就提高了性能
    但是,流水线要是中断的话,性能损失就会很大,就需要从头开始进行执行
    指令重排就是为了尽量少的中断流水线

哪些指令不能重排:Happen-before规则

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile规则:volatile变量的写,先发生于读,保证了volatile的可见性
  • 锁规则:解锁必然发生在随后的加锁前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断先于被中断线程的代码
  • 对象的构造函数执行,结束先于finalize()方法

参考书籍

《Java高并发程序设计》-------------------------葛一鸣,郭超著