本文介绍sun.misc.Unsafe
类,它是实现Java并发包的基石类之一。
分4个类别进行介绍:
- CAS操作
put*Volatile
和get*Volatile
put*
和get*
putOrderedInt
,putOrderedLong
,putOrderedObject
一、前言
1.1、volatile变量内存语义
参见《synchronized-volatile-final关键词》,主要包括3个方面:
- volatile变量的简单读写操作是原子的
- 具有volatile变量写语义,即“对变量的写对后续的读立即可见”
- 满足volatile变量happens-before规则
1.2、以“直接操作变量内存地址”形式操作变量
本文所述方法以“直接操作变量内存地址”形式操作变量,跟“一般直白”形式操作变量不同。
对于是否有volatile变量内存语义,两者的区别是:
- 对于“一般直白”形式,以变量是否由volatile修饰决定,比如“代码片段1中的//1和//2分别具有volatile变量内存语义,//3和//4不具有volatile变量内存语义”
- 对于“直接操作变量内存地址”形式,不能感知是否由volatile修饰,故不能以变量是否由volatile修饰决定,只能以“方法设计意图是否提供volatile变量内存语义”决定,比如“代码片段2中//3和//4分别具有volatile变量内存语义,//1和//2不具有volatile变量内存语义”
代码片段1:
1 | class VolatileExample { |
代码片段2:
1 | class VolatileExample { |
1.3、几种操作形式得到的volatile变量内存语义是等价和可互操作的
几种操作形式得到的volatile变量内存语义是等价和可互操作的,操作形式有:
- 以“一般直白”形式进行volatile变量操作
- 本文所指的3个CAS操作
-put*Volatile
和get*Volatile
方法
二、CAS操作
有3个CAS操作:compareAndSwapInt
,compareAndSwapLong
和compareAndSwapObject
。
上述CAS操作,具有volatile变量内存语义,即:
- 3个方法是原子的
- 具有volatile变量写语义,即“对变量的写对后续的读立即可见”
- 能够满足volatile变量happens-before规则
接下来证明CAS操作具有volatile变量内存语义。以compareAndSwapInt
方法为例,在X86指令架构中,跟踪其本地实现源代码如下:
1 | static JNINativeMethod methods_15[] = { |
1 | Unsafe_CompareAndSwapInt( JNIEnv * env, jobject unsafe, jobject obj, jlong offset, jint e, jint x ) ) { |
1 | inline jint Atomic::cmpxchg( jint exchange_value, volatile jint* dest, jint compare_value ) |
可知在执行compareAndSwapInt
方法时,最后会执行到LOCK_IF_MP( % 4 ) "cmpxchgl %1,(%3)"
语句,其中LOCK_IF_MP( % 4 )
表示在单CPU且单核的环境中,可以优化不加LOCK指令前缀
;否则就需要加LOCK指令前缀
以确保cmpxchgl
的原子性,而cmpxchgl
表示一个属于CMPXCHG
家族的CAS操作指令,它的末尾l
表示操作数长度为4。
如果是单核情形,如上所述——优化不加LOCK指令前缀
,接下来证明的确实现volatile变量内存语义:
- 在单核情形中,
cmpxchgl
指令本身是原子的 - 在单核情形中,对变量的写对后续的读立即可见
- 根据《synchronized-volatile-final关键词》,为实现volatile变量happens-before规则,还需按照方案1添加内存屏障,但是在X86指令架构中,上述内存屏障对应的重排序情形不被允许,故不需要按照方案1添加内存屏障
如果是多核情形,加上LOCK指令前缀
,接下来证明的确实现volatile变量内存语义:
- 在多核情形中,
LOCK + cmpxchgl
是原子的 - 在多核情形中,
LOCK指令前缀
使得“刷新缓存到内存”和“使缓存失效”,故“对变量的写对后续的读立即可见” - 根据《synchronized-volatile-final关键词》,为实现volatile变量happens-before规则,还需按照方案1添加内存屏障,但是在X86指令架构中,上述内存屏障对应的重排序情形不被允许,故不需要按照方案1添加内存屏障;即便不是这样,
LOCK指令前缀
使得禁止重排序
,也就等价于已按照方案1添加内存屏障
方案1:
- 在每个volatile写操作的前面插入一个LoadStore内存屏障
- 在每个volatile写操作的前面插入一个StoreStore内存屏障
- 在每个volatile读操作的后面插入一个LoadLoad内存屏障
- 在每个volatile读操作的后面插入一个LoadStore内存屏障
三、put*Volatile
和get*Volatile
我们知道,在Java中有8个基本类型和1个引用类型,因此有9个相应的put*Volatile
和get*Volatile
方法。
9个相应的put*Volatile
方法:
- putBooleanVolatile
- putByteVolatile
- putCharVolatile
- putShortVolatile
- putIntVolatile
- putFloatVolatile
- putLongVolatile
- putDoubleVolatile
- putObjectVolatile
9个相应的get*Volatile
方法:
- getBooleanVolatile
- getByteVolatile
- getCharVolatile
- getShortVolatile
- getIntVolatile
- getFloatVolatile
- getLongVolatile
- getDoubleVolatile
- getObjectVolatile
上述方法,具有volatile变量内存语义(证明过程略),即:
put*Volatile
方法是原子的,具有volatile变量写语义,能够满足volatile变量happens-before规则get*Volatile
方法是原子的,具有volatile变量读语义,能够满足volatile变量happens-before规则
四、put*
和get*
我们知道,在Java中有8个基本类型和1个引用类型,因此有9个相应的put*
和get*
方法。
9个相应的put*
方法:
- putBoolean
- putByte
- putChar
- putShort
- putInt
- putFloat
- putLong
- putDouble
- putObject
9个相应的get*
方法:
- getBoolean
- getByte
- getChar
- getShort
- getInt
- getFloat
- getLong
- getDouble
- getObject
上述方法,不具有volatile变量内存语义(证明过程略),具体是:
- 操作不一定是原子的
- 对变量的写操作不具有volatile变量写语义,而只是普通的变量写语义
- 不满足volatile变量happens-before规则
五、putOrderedInt
,putOrderedLong
和putOrderedObject
有3个方法:putOrderedInt
,putOrderedLong
和putOrderedObject
。
上述方法与“volatile变量内存语义”的契合情况描述如下:
- 对于写操作是原子的
- 对变量的写操作不具有volatile变量写语义,即“对变量的写不能对后续的读立即可见”。具有volatile变量写语义的操作(上文中的“以‘一般直白形式’写volatile变量”,“3个CAS操作中的写部分”,“
put*Volatile
方法”)在实现写时为使具有volatile变量写语义,需要添加一个绑定型内存屏障,该绑定型内存屏障用来使得“刷新缓存到内存,并使相应的缓存失效”,而putOrderedInt/putOrderedLong/putOrderedObject
方法没有添加该绑定型内存屏障,故不具有volatile变量写语义。当然有失必有得,后者相对于前者有更好的写性能,这也是后者被设计出来的原因,后者常见的一个应用场景是“一系列写操作之后,统一作一个可见性确保操作,再一个读取操作” - 根据《synchronized-volatile-final关键词》博文中“二、实现概述”小节内容我们知道,为实现
volatile变量happens-before规则——“对一个volatile域的写,happens-before于任意后续对这个volatile域的读”
,需要满足“实现volatile关键词内存语义”下的第2和第3个条件,但是从实际使用该happens-before规则的角度——如果“对这个volatile域的读”,能够读到“对该volatile域的写”,那么“对这个volatile域的读”就是“后续”的
,只需要满足第3个条件即可,前者需要满足第2个条件是因为“否则后续的读不保证能读到前面的写”,我们称前者为“狭义的volatile变量happens-before规则”,后者为“广义的volatile变量happens-before规则”。在这里,以上3个方法在写操作前会插入一个LoadStore和一个StoreStore内存屏障,因此如果配对“具有volatile变量读语义的操作”就能应用“广义的volatile变量happens-before规则”
备注:
- 关于以上“广义的volatile变量happens-before规则”的论述,属于笔者的自我理解,如有错误敬请指教。Stack Overflow上这个回答中认为以上3个方法不满足“volatile变量happens-before规则”,是因为他们所指的是“狭义的volatile变量happens-before规则”,故需要写入具有volatile变量写语义,而以上3个方法不具有volatile变量写语义
- 支持笔者自我理解的证据有:
- 查看JDK 16中Unsafe类源码,
putOrderedObject
方法的JavaDoc中有一句——Corresponds to C11 atomic_store_explicit(..., memory_order_release)
,再结合下述资料即可证明论述(当然这里同时也证明了上述3个方法的写操作是原子的):资料1,资料2,资料3,资料4 - 查看JDK 16中FutureTask类源码,
set
方法实现中转发调用VarHandle类的setRelease
方法(在JDK 1.8中是转发调用Unsafe类的putOrderedInt
方法,自然可推断两个方法是等价的),setRelease
方法的JavaDoc中有一句——Ignoring the many semantic differences from C and C++, this method has memory ordering effects compatible with {@code memory_order_release} ordering.
,结合以上第1点,论述得证
- 查看JDK 16中Unsafe类源码,
参考文献
[1]http://psy-lob-saw.blogspot.com/2012/12/atomiclazyset-is-performance-win-for.html
[2]https://stackoverflow.com/questions/1468007/atomicinteger-lazyset-vs-set
[3]http://ifeve.com/how-does-atomiclong-lazyset-work/
[4]https://stackoverflow.com/questions/58185339/does-storestore-memory-barrier-in-java-forbid-the-read-write-reordering
[5]https://github.com/fengjiachun/doc/blob/master/concurrent/%E6%B5%85%E6%9E%90JUC%E4%B8%ADAtomic%20class%E7%9A%84lazySet.md
[6]https://blog.csdn.net/u010597819/article/details/113922471
[7]《happens-before规则》
[8]https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6275329