java原子性操作
1. long和double类型操作的非原子性探究
深入Java虚拟机中提到,对于小于等于32位的基本类型如int,其操作是原子性的。然而,对于long和double类型,某些32位的Java虚拟机并未提供原子性操作,这可能导致错误数据的产生。相反,64位的Java虚拟机对long和double变量的处理是原子的。
在32位的Java虚拟机中,long和double变量被视为两个32位的原子值,而非一个64位的原子值。这意味着在将long类型值保存至内存时,可能存在两次32位的写操作。若两个竞争线程分别尝试写入不同的值至内存,则可能形成错误的内存值。当多个线程操作同一个变量时,流程大致分为几个步骤。
例如,若执行顺序为(1)(4)(3)(2),则内存中的值将由线程1的高32位和线程2的低32位组成,从而形成一个错误的组合值。
在实际测试中,发现程序在while循环时陷入死循环,这表明所使用的JVM为64位。在64位JVM中,double和long的赋值操作是原子的。然而,当使用32位JVM时,程序能跳出循环,表明赋值过程被拆分,无法保证原子性。
为了解决这一问题,可以查阅Oracle Java Spec文档。文档中提到,某些Java实现可能将64位long或double值的写操作划分为两次相邻的32位值写操作,以追求更高的效率。然而,这一行为是实现可以自行决定的,Java虚拟机可以自行决定是否原子性地对待long和double值的写操作。考虑到内存模型的目的,将非volatile的long或double值的单次写操作视为两次分开的写操作(每次32位),可能会导致某个线程只读取到一次写操作中的前32位,而另一部分写操作的后32位则未被读取。对于volatile的long和double值的读写操作总是原子的,且对于引用的操作,不论其实现采用32位还是64位,总能保持原子性。
值得注意的是,volatile只能保证可见性,而非原子性。但是,通过将long和double变量声明为volatile,可以确保其操作的原子性。因此,鼓励虚拟机实现者避免将64位值的写操作拆分为两次,同时建议编码人员将共享的64位值声明为volatile,或者正确同步程序以避免可能的并发问题。