0%

Unsafe类

本文介绍sun.misc.Unsafe类,它是实现Java并发包的基石类之一。
分4个类别进行介绍:

  • CAS操作
  • put*Volatileget*Volatile
  • put*get*
  • putOrderedIntputOrderedLongputOrderedObject

一、前言

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class VolatileExample {
volatile int a;

int b;

public int getA() {
return a; //1
}

public void setA(int a) {
this.a = a; //2
}

public int getB() {
return b; //3
}

public void setB(int b) {
this.b = b; //4
}
}

代码片段2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class VolatileExample {
private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueAOffset;
private static final long valueBOffset;

static {
try {
valueAOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("a"));

valueBOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("b"));
} catch (Exception ex) {
throw new Error(ex);
}
}

volatile int a;
int b;

public int getA() {
return unsafe.getInt(this, valueAOffset); //1
}

public void setA(int a) {
unsafe.putInt(this, valueAOffset, a); //2
}

public int getB() {
return unsafe.getIntVolatile(this, valueBOffset); //3
}

public void setB(int b) {
unsafe.putIntVolatile(this, valueBOffset, b); //4
}
}

1.3、几种操作形式得到的volatile变量内存语义是等价和可互操作的

几种操作形式得到的volatile变量内存语义是等价和可互操作的,操作形式有:

  • 以“一般直白”形式进行volatile变量操作
  • 本文所指的3个CAS操作
  • put*Volatileget*Volatile方法

二、CAS操作

有3个CAS操作:compareAndSwapIntcompareAndSwapLongcompareAndSwapObject
上述CAS操作,具有volatile变量内存语义,即:

  • 3个方法是原子的
  • 具有volatile变量写语义,即“对变量的写对后续的读立即可见”
  • 能够满足volatile变量happens-before规则

接下来证明CAS操作具有volatile变量内存语义。以compareAndSwapInt方法为例,在X86指令架构中,跟踪其本地实现源代码如下:

1
2
3
4
5
6
static JNINativeMethod methods_15[] = {
/* 省略一堆代码... */
{ CC "compareAndSwapInt", CC "("OBJ "J" "I" "I" ")Z", FN_PTR( Unsafe_CompareAndSwapInt ) },
{ CC "compareAndSwapLong", CC "("OBJ "J" "J" "J" ")Z", FN_PTR( Unsafe_CompareAndSwapLong ) },
/* 省略一堆代码... */
};
1
2
3
4
5
6
Unsafe_CompareAndSwapInt( JNIEnv * env, jobject unsafe, jobject obj, jlong offset, jint e, jint x ) ) {
UnsafeWrapper( "Unsafe_CompareAndSwapInt" );
oop p = JNIHandles::resolve( obj );
jint * addr = (jint *) index_oop_from_field_offset_long( p, offset );
return( (jint) (Atomic::cmpxchg( x, addr, e ) ) == e);
}
1
2
3
4
5
6
7
8
9
inline jint Atomic::cmpxchg( jint exchange_value, volatile jint*     dest, jint compare_value )
{
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP( % 4 ) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return(exchange_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*Volatileget*Volatile

我们知道,在Java中有8个基本类型和1个引用类型,因此有9个相应的put*Volatileget*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规则

五、putOrderedIntputOrderedLongputOrderedObject

有3个方法:putOrderedIntputOrderedLongputOrderedObject

上述方法与“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点,论述得证

参考文献

[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

您的支持将鼓励我继续分享!