Java并发之volatile详解 (2)

可见性

lock指令的早期实现会对总线进行锁定,锁定期间其他处理器对内存的读写都被阻塞,直到锁定释放。由于锁总线的开销太大,因此后期使用缓存锁代替总线锁。

有序性原理

lock指令由于锁的存在,对缓存的读一定在写之后,lock也因此暗含了一定有序性的保证。而在整体代码指令上的有序性,是由内存屏障保证的,lock并不能提供内存屏障

JSR-133将读写分为:

普通读:非volatile字段的读取如getField,getStatic或数组加载

普通写:非volatile字段的写如setField,setStatic或数组存储

volatile读:多线程可达的volatile字段读取

volatile写:多线程可达的volatile字段

JMM对四种读写制定了重排规则:

volatile重排规则

根据该表有以下语义:

第一个操作为volatile读,后续任何读写都不得重排到该操作之前

第二个操作为volatile读,前序volatile读写不得重排到该操作之后

第一个操作为volatile写,后续volatile读写不得重排到该操作之前

第二个操作为volatile写,前序任何读写都不得重排到该操作之后

为了实现有序性语义,JMM提供了四种内存屏障

内存屏障 指令序 说明
StoreStore屏障   Store1;StoreStore:Store2   确保Store1数据对其他处理器可见先于Store2及所有后续存储指令  
StoreLoad屏障   Store1;StoreLoad;Load2   确保Store1数据对其他处理器可见先于Load2及后续加载指令  
LoadLoad屏障   Load1;LoadLoad;Load2   确保Load1数据装载先于Load2及所有后续装载指令  
LoadStore屏障   Load1;LoadStore;Store2   确保Load1数据装载先于Store2及后续所有存储指令  

volatile基于内存屏障禁止指令重排序,实际上发现一个最小化插入内存屏障的总数几乎是不可能的,JMM采取了保守策略,主要遵循以下规则:

在每个volatile写之前插入StoreStore屏障,确保volatile写之前的写操作不会被重排到volatile写之后

在每个volatile写之后插入StoreLoad屏障,确保volatile写之后的读写操作不会重排到volatile写之前

在每个volatile读之后插入LoadLoad屏障和LoadStore屏障,禁止下面所有读写操作重排到volatile读之前

为什么在volatile写之前没有加入LoadStore保证读不能重排在写后面呢?

个人理解是因为volatile的lock指令限制了写优先于读,因此省略了该屏障。

实际执行时,编译器会根据具体情况省略不必要的屏障,如下面的示例:

int a; volatile int v, u; void f() { int i, j; i = a; // load a i = v; // load v // LoadLoad 因为store a不可能越过load u,可省略LoadStore j = u; // load u // LoadStore 因为下面没有读,可省略LoadLoad a = i; // store a // StoreStore v = i; // store v // 因为紧跟一个volatile写,省略StoreLoad交给store u添加 // StoreStore u = j; // store u // StoreLoad s i = u; // load u // LoadLoad 防止下面的load a重排 // LoadStore 防止下面的store a重排 j = a; // load a a = i; // store a } 先行发生原则 happen-before

如果Java中所有的有序性都靠添加volatile和synchronized来保证,会使得程序编写非常啰嗦。但我们在编写Java程序时并没有感受到这一点,是因为Java语言的先行发生原则happen-before。

先行发生原则通俗解释即,如A操作产生的影响对B操作有效,那么A应当是先行发生于B。先行发生的概念不难理解,假如没有先行发生的约束,会出现什么问题呢?

boolean configured = false; // Thread1 while (!configured) {} doSometing(); // Thread2 loadConfig(); configured = true;

看一段示例代码,Thread1需等待Thread2加载配置完毕后,才能继续往下执行,于是逻辑上Thread2的操作应当先行发生于Thread1。在Thread2中,loadConfig应当先行发生于configured=true。如果没有先行发生原则,由于指令重排的存在,Thread2可能先执行了configured = true然后才加载配置,而Thread1已读取到configured:true,程序就会发生不可预知的错误。

Java内存模型定义了几条天然的先行发生关系,如两个操作不在以下范围内,则不保证操作间的顺序执行。

程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。

管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。

volatile变量规则对一个volatile修饰的变量,对他的写操作先行发生于读操作。

线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。

线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。

线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。

对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzdfwy.html