Shared variables是指共享变量,多个线程共享的内存区域被称之为共享内存或者堆内存,而共享变量的地址空间位于该区域之内。在JVM中,所有类实例的域、类的静态域以及数组的元素是存储在堆内存中,这些域与数组元素统称为变量。
本地变量、方法的形式参数以及异常处理块的参数永远不会在多线程之间共享,它们也不会受内存模型的影响。如果读操作或者写操作同时访问同一个变量,至少一个操作是写操作,则会发生冲突的情况。
一个线程交互的动作是指一个线程执行的动作,该动作也可以被其他线程探测到或者直接影响,执行一段程序时可能发生的线程交互的动作如下所示:
-- Volatile读,读取一个Volatile类型的变量 -- Volatile写,写入一个Volatile类型的变量 -- 上锁(Lock),对一个监听器增加锁 -- 解锁(Unlock),对一个监听器释放锁 -- 一个线程的开始动作或者结束动作(由编译器自动生成的动作) -- 启动一个线程的动作或者探测一个线程是否已经被终止的动作
|
线程交互的动作是在多线程环境中执行,因而不需要关注单个线程内部的动作(例如两个本地变量相加,并将结果保存在第三个本地变量中)。由前面所述的内存模型可知,所有线程都需要遵守Java程序语言定义的线程内部的正确语义,在Java规范中,通常把线程交互的动作简称为一个动作,一个动作a由一个四元组
--对于上锁动作,v表示执行上锁操作的监听器(monitor) --对于解锁动作,v表示执行释放锁操作的监听器(monitor) --对于读动作(volatile或者非volatile),v是被读入的变量 --对于写动作(volatile或者非volatile),v是被写入的变量
|
另外,对于外部动作,其四元组包括一个额外的组件,该组件包含一个外部动作的结果,该结果是由执行该外部动作的线程所观察到或者感知到,因此,该结果可能是一些与外部动作有关的成功信息或者失败信息,或者是一些由外部动作读取到的值,外部动作的四元组并不包括外部动作的参数(例如,那些写入到socket的字节),诸如此类的参数是由线程内部的其他动作负责设置,以及使用线程内部的语义检测这些参数的合法性。
Programs and Program Order是指程序与程序顺序,在线程t执行的所有线程交互的动作中,t的程序顺序是一个总顺序,该顺序反映出所有动作的执行顺序,该顺序也必须遵守t的线程内部的正确语义。
如果一个动作集合以一个总的执行顺序发生,并且这些动作是与程序顺序保持一致的,则这些动作集合是满足顺序一致性的,另外,每个对变量v的读入动作r,都可以见到任何对变量v的写入动作w的写入值,则r与w需要符合的条件如下所示:
|
顺序一致性能非常有效地保证一段程序的执行顺序以及可见性,在一次顺序一致的执行过程中,所有独立的动作保持一个总的执行顺序,该执行顺序与程序顺序是一致的,而每个独立的动作是原子操作、对每一个线程是即时可见。
假如一段程序不存在数据竞争,则该段程序内所有执行的动作是满足顺序一致性的,然而,顺序一致性、以及数据的自由竞争也允许在一组操作中出现错误,这些错误可以被自动感知或者不被感知,依实际情况而定。
综上所述,假如采用顺序一致性作为内存模型,则前面章节描述的有关编译器以及处理器的很多优化就变得非法或者变得不符合逻辑,在上例的程序代码中,只要p.x的值发生变化,根据顺序一致性的原则,则后面的代码中涉及到p.x的值都必须跟随变化。
Synchronization Order是指同步顺序,一个同步顺序是指一次执行过程中所有同步动作的总顺序,对于每个线程t,t内所有同步动作的同步顺序是与t的程序顺序保持一致,同步动作会引发动作的同步关系(synchronizes-with),同步动作的定义如下所示:
--虽然在为变量申请空间之前对这些变量赋予默认值不太符合逻辑,但是每个对象被创建时都会初始化默认值
--例如,在该使用场景中,T2会调用T1.isAlive() or T1.join()
--T2的中断状态可以使用这些方法确定,直接抛出InterruptedException异常信息,或者调用Thread.interrupted或者Thread.isInterrupted |
综上所述,假如A动作与B动作发生同步关系,其中A与B交互的面被称之为同步关系的边缘,则A对应的是源资源,A动作被称之为一次释放(release),则B对应的是目标资源,B动作被称之为一次获取(acquire)。
Happens-before Order是一种顺序,该顺序表示的是两个动作之间发生happens-before的逻辑关系,动作-1 happens-before 动作-2,则动作-1对于动作-2是可见的、以及动作-1排列在动作-2之前。假设存在两个动作x、y,数学函数的形式表示为hb(x,y),其意义是指x happens-before y,做出如下的定义:
|
类Object的wait方法包括上锁以及解锁的动作,wait方法中的上锁动作与解锁动作符合happens-before顺序的关系。另外,假设两个动作符合happens-before顺序关系,则这两个动作的执行顺序并不一定需要与代码的实现顺序保持一致,也就是,只要JVM执行指令重排序之后的执行结果是合法,则重排序之后的顺序也是合法。例如,线程t构造了一个object对象o,动作a写入初始值到o中,t的启动动作是b,a与b是happens-before的逻辑关系,但是a不一定需要发生在b之前,如果不涉及其他读操作,则a可以发生在b之后。
在多线程的使用场景中,存在着数据竞争的情况,即其中一个线程对竞争数据执行写入操作,而同时其他线程对竞争数据执行读入操作,最终导致被读入的值很可能是乱序。因此,happens-before顺序关系也对在什么时间点发生数据竞争作出定义。
假设存在一个同步的边缘集合S,而且集合S是最小的充分集,其中,集合S中每个元素是一个二元组,其形式是(x,y),表示x动作与y动作发生同步关系,当集合S中所有二元组元素满足传递闭包的程序顺序关系的时候,则集合S可以唯一确定所有符合happens-before顺序关系的边缘集合,做出如下的定义:
|
由以上的分析可知,当一段程序包括两个冲突的访问动作,并且这两个动作不符合happens-before顺序关系,则这种场景肯定存在数据竞争。有些操作在语义上不是线程交互的动作,例如,读取数组的长度、执行类型检查、调用虚拟的方法,这些操作不会直接受到数据竞争的影响,因而,数据竞争不会引发诸如读取数组错误长度的不正确行为。
一段程序是正确地同步,当且仅当所有按照顺序一致性的执行都不受到数据竞争的影响,也就是,一段程序是正确地同步,则这段程序所有的执行都是符合顺序一致性的。
正确地同步,这一性质对于程序员来说是一种相当强大的保证,程序员不需要花太多的时间与精力去推理重排序与引发数据竞争的问题,也就是说,一旦确认代码是正确地同步,则程序员不需要担心重排序会影响代码的执行结果,在多线程的并发环境中,正确地同步可以避免很多因为代码重排序而引发的违反直觉的代码行为,例如,存在多个读写线程同时对相同的变量执行读写操作,则某一个线程可能读到的值并非是代码所期望的值,这就违反了代码的直觉行为,而正确地同步能为程序员提供一种简单而有效的方法去解决此类数据竞争的问题。
假设存在变量v,r是对v读入动作,w是对v写入动作,r允许可见w,则执行轨迹需要符合如下两者之一的条件:
|
由以上的分析可知,如果不存在happens-before顺序关系阻止r的动作,则r总是可见w。假设存在动作集合A,A是符合happens-before一致性,对于A中的所有读动作r,存在写入动作W(r),使得r可见W(r),则或者不符合hb(r,W(r))顺序关系,或者不符合:A中存在写入动作w,其中w.v=r.v,使得满足hb(W(r),w)与hb(w,r)的顺序关系。因此,对于A中的动作,可以运用happens-before顺序关系,使得每个读入动作r可见一个写入动作w。如下举例说明在无同步的情况下,代码中发生happens-before一致性的问题:
| Thread 1 B = 1; r2 = A; |
| Thread 2 A = 2; r1 = B; |
| 在线程1与线程2中,A与B初始化值等于0,如果没有运行同步关系,则在多线程并发的情况下,资源调度的顺序由系统确定,而与代码的顺序无关,因此,代码的执行轨迹可能如下所示: 1: B = 1; 3: A = 2; 2: r2 = A; // 读取到可见的初始化写入值A=0 4: r1 = B; // 读取到可见的初始化写入值B=0 |
| 运用happens-before一致性顺序关系(hb(r,w))约束的情况下,代码的执行轨迹可能如下所示: 1: r2 = A; // 读取到可见的写入值A=2 3: r1 = B; // 读取到可见的写入值B=1 2: B = 1; 4: A = 2; 由以上多线程的执行结果可知,happens-before一致性的原则允许读取到随后写入的值,从代码的角度看,该方式违反了直觉,往往会产生不可接受的行为。 |
(未完待续)