首先我们先谈谈什么是数据竞争,
数据竞争的定义:
1 在一个线程正在写一个变量
2 在一个线程中正在读一个变量
3 变量的写和读没有通过同步来进行排序
当程序中存在数据竞争的时候,往往会产生违背直觉的一些结果,在一个多线程程序中,如果进行了正确的同步,那么这将是一个没有数据竞争的程序。
JMM对于正确同步的多线程程序做了内存一致性的保证:
如果一个多线程程序是正确同步的,那么程序的执行就具有顺序一致性——即程序的执行结果与在顺序一致性模型中的执行结果一致,这里的同步是指广义上的同步:synchronized、volatile和final
下面将从这几方面介绍熟悉怒一致性模型:什么是顺序一致性模型、同步程序的顺序一致性模型、非同步程序的顺序一致性模型
1 顺序一致性模型:
顺序一致性模型的定义:
在顺序一致性模型中,程序完全按照程序的顺序执行。
顺序一致性模型的两大特性:
1 一个程序中的所有操作都必须按照程序的顺序来执行
2 不论程序是否同步,所有的线程只能看到一个单一的程序操作执行顺序,同步还要保证程序的执行立刻对所有线程可见
在概念上来讲,顺序一致性模型中有一个单一的全局内存,每一个线程都必须按照程序的顺序来对内存进行读写操作,也就是在任意时间点,只有一个线程可以连接到这个内存,如果是多线程环境,那么顺序一致性模型会保证程序在多个线程之间是串行执行的,例如:JMM通过同步的方式来保证这一点
借用书中的图来说明这一点吧,下图为一个多线程环境,两个线程通过监视器锁来进行同步,来达到顺序一致性模型的效果
对于未同步的程序来说,可能是下面这样的,
虽然这个程序在顺序一致性模型中整体上是无序的,但是对于所有线程而言,都会看到一个一致的整体程序执行顺序,之所以这样,是因为顺序一致性模型保证每个操作立即对所有线程可见
可以这样想:
假设每个操作不对线程立即可见,假设A线程修改了变量i,并没有立即将其从工作内存刷新到主内存,那么B线程就看不到i的变化,那么A线程和B线程所看到的程序的整体执行顺序就不是一致的
A A1 A2 A3
B B1 B2 B3
a 立即可见的操作:
A1 B1A2 B2 A3 B3
b 不立即可见的操作:
B1 A1 A2 B2 A3B3
JMM里面就没有这个保证,不但整体无序,而且各个线程看到执行顺序也不一致
2 同步程序的顺序一致性
那么同步程序是怎么实现顺序一致性的呢,
package com.thread.demo;
/**
* Created by jiangry01 on 2018/9/6.
*/
public class SynchronizedDemo {
int a = 0;
boolean flag = false;
public synchronized void write(){//获取锁
a = 1;
flag = true;
} //释放锁
public synchronized void read(){ //获取锁
if(flag)
int i = a;
} //释放锁
}
看上面这段代码,通过synchronized关键字对程序进行同步,这是一个正确同步的多线程程序,如果A线程执行write方法,B线程执行read方法,根据JMM规范,改程序与在顺序一致性模型中的执行结果相同,下面我们将两种情况下做下对比:
1 JMM中进行同步的情况
步骤1:A线程获取锁,此时A线程进入临界区
步骤2: i=1
步骤3: flag=true
注意:JMM中允许对程序进行重排序,也就是2、3的执行顺序可能是无序的,但是这并不影响程序最终的结果,因为程序进行了同步(由于监视器锁的原语的原因,不允许临界区的代码‘逸出’到临界区之外,而且监视器是互斥的),在进入和退出临界区的时候,JMM会做一些特别处理,来保证内存的可见性,虽然临界区内做了重排序,但是由于监视器锁的互斥性,线程B看不到重排序,这样,这样既提高了程序的运行效率,又没有改变程序的执行结果。
步骤4: if(flag)
flag=true
步骤5: i = 1
4、5也可以进行重排序
总结:
对于正确同步的程序,JMM在实现上的基本方针:
在不改变程序的前提下,尽可能为编译器和处理器的优化提供方便。
2 顺序一致性模型中的情况
步骤1:线程A获取锁
步骤2: i = 1;
步骤3: flag=true;
步骤4: 线程A释放锁
步骤5:线程B获取锁
步骤6:if(flag)
步骤7:int i=a;
步骤8::线程B释放锁
可见,顺序一致性模型里面,是完全按照程序的顺序来执行的
3 未同步程序的顺序一致性
对于未同步的程序或者是同步不正确的程序,JMM只保证最小安全性,即保证线程所读取的值要么是之前某个线程写入的值,要么是默认的值(0,null,false),JMM保证线程所读取到的值不会无中生有的出来,那JMM是怎么保证最小安全性的呢?
在堆中分配对象的时候,首先会对内存空间进行清零, 然后再在内存空间上分配对象,这两个动作会同步的进行,这样,就完成了域的默认初始化。
那么未同步程序在两个模型里有什么差别呢?
1 单线程情况下,在JMM模型里面,可能会发生重排序,而在顺序一致性模型里,会按照程序的顺序执行
2 多线程情况下,在JMM模型里,各个线程看到的程序执行顺序可能不是一致的(由于内存可见性导致),而在顺序一致性模型里,所有的线程都会看到一致的程序执行顺序,因为顺序一致性模型保证内存可见性
3 JMM不保证对64位的long和double变量的写操作具有原子性,而顺序一致性模型保证对任何类型变量的写操作具有原子性
第3个差异和处理器总线的工作机制有关系
那么什么是计算机总线呢?
在计算机中,数据通过总线在处理器和内存之间进行传递,也就是通过总线事务来完成的,总线事务分为两种:写事务和读事务。
写事务将数据从处理器传递到内存,读事务将数据从内存传递到处理器,读/写事务会处理在物理上一系列连续的字,在一个处理器处理总线事务期间,会禁止其他处理器的总线事务请求,总线的这些处理机制会保证处理器对内存的读写是串行化的任意时间点,只有一个处理器可以读写内存,这样的特性保证了单个总线事务对内存的读/写具有原子性。
但是对于32位处理器,对64位的数据的读写并不具备原子性,因为jvm认为拆成2个32位的操作比较方便,会把一个64位long/double型数字的读/写拆分成两个32位的读/写的操作,那么这两个32位的操作就会可能被分配到两个不同的总线事务执行.
可能会造成long/double的高低32位数据不一致