原创:临虹路365号(微信公众号ID:codegod365),欢迎分享,转载请保留出处。
我们在阅读基础库源码时,经常会碰到AtomicReferenceFieldUpdater
相关的使用,例如lazySet()
、set()
、compareAndSet()
等方法的使用,其底层是对unsafe
使用包装,本文以对理解AtomicReferenceFieldUpdater
为引子,深入学习下unsafe
中对volatile的一些使用方法以及相关作用。
AtomicReferenceFieldUpdater例子
以Project Reactor
项目中的reactor.core.scheduler.ParallelScheduler
为例,其声明了一个volatile变量executors
,同时也声明了一个静态常量AtomicReferenceFieldUpdater类型的EXECUTORS
。我们看到EXECUTORS
的传参就是变量executors
的一个反射代理,其效果是实现了类似AtomicReference的效果
声明举例
volatile ScheduledExecutorService[] executors;
static final AtomicReferenceFieldUpdater<ParallelScheduler, ScheduledExecutorService[]> EXECUTORS =
AtomicReferenceFieldUpdater.newUpdater(ParallelScheduler.class, ScheduledExecutorService[].class, "executors");
使用举例
void init(int n) {
ScheduledExecutorService[] a = new ScheduledExecutorService[n];
for (int i = 0; i < n; i++) {
a[i] = Schedulers.decorateExecutorService(this, this.get());
}
EXECUTORS.lazySet(this, a);
}
public void dispose() {
ScheduledExecutorService[] a = executors;
if (a != SHUTDOWN) {
a = EXECUTORS.getAndSet(this, SHUTDOWN);
if (a != SHUTDOWN) {
for (ScheduledExecutorService exec : a) {
exec.shutdownNow();
}
}
}
}
其使用方式,其实和AtomicReference几乎一致,例如getAndSet、lazySet、compareAndSet等等方法。
那为什么需要AtomicReferenceFieldUpdater,而不直接使用AtomicReference呢? 其原因是:
发现AR比ARFU写起来的时候代码量要少得多,而且AR不用像ARFU一样需要用到反射,但是很多框架却选择使用ARFU,我们查看源码发现,AR源码里面,本质也有一个private volatile V value; 存在,那么这两者的差异点主要在于AR本身是要指向一个对象的,也就是要比ARFU多创建一个对象,而这个对象的头(Header)占12个字节,它的成员(Fields)占4个字节,也就比ARFU要多出来16个字节,这是对于32位的是这种情况,如果是64位的话,你启用了-XX:+UseComparessedOops 指针压缩的话,那么Header还是占用12个字节,Fields也还是占用4个字节,但如果没有启用指针压缩的话,那么Header是占16个字节,Fields占用8个字节,总共占用24个字节,那么就说明每创建一个AR都会多出来这么多的内存,那么对GC的压力就有很大的影响了。
在基础库中,由于在程序中会被频繁使用,每个对象能节省16字节能够优化很大性能,比如可以大量减少垃圾的产生,降低GC的频率。特别是像AtomicXXX的类,其成员变量就一个,比如AtomicInteger,其本身就一个int型变量占4个字节,而却要额外付出16字节的成本,浪费比例占比达75%。
lazySet理解
我们可以看到,在AtomicXXX的类中,除了常见的getAndSet、compareAndSet、set等方法外,还有一个特殊的方法,叫lazySet,那这个是什么呢?与set方法有什么区别呢?
java中java.util.concurrent.atomic
包其实底层用的是unsafe
的类,以Object
类型为例,对于变量的内存操作,主要有以下几个操作:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public native Object getObjectVolatile(Object var1, long var2);
public native void putObjectVolatile(Object var1, long var2, Object var4);
public native void putOrderedObject(Object var1, long var2, Object var4);
public final Object getAndSetObject(Object var1, long var2, Object var4) {
Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while(!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;
}
注意:在JDK9开始,unsafe已经可以用VarHandle(变量句柄)类型来替代,其提供的功能对内存模型更友好更高效。详见:openjdk.org/jeps/193
可以看到,对于写操作,unsafe
提供了两个方法putObjectVolatile
和putOrderedObject
,其分别对应了AtomicReferenceFieldUpdater
中的set和lazySet方法。
public final void set(T obj, V newValue) {
accessCheck(obj);
valueCheck(newValue);
U.putObjectVolatile(obj, offset, newValue);
}
public final void lazySet(T obj, V newValue) {
accessCheck(obj);
valueCheck(newValue);
U.putOrderedObject(obj, offset, newValue);
}
那么putObjectVolatile
和putOrderedObject
到底区别是什么呢? 要回答这个问题,我们需要先对volatile修饰符理解。
volatile的理解
我们知道,在多线程编程中对变量操作时,需要注意三个问题,即保证程序的原子性
、顺序性
、可见性
,为此java在JMM模型中,提出了happens-before
的原则,以帮助程序员在并发编程时的实现。其中happens-before
原则在变量相关的有:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
可以看到只有在锁定
或volatile
的情况下,才会有happens-before
原则的遵守,普通变量则不会保证happens-before
。
对于锁定
规则,其实就是Java中的锁实现,有synchronize和Lock两种方式,其中Lock使用的则是unsafe
下的park
与unpark
。在用锁
的情况下,同时满足了原子性
、顺序性
、可见性
三大特性,但其使用代价过高,特别是有些时候对变量的读写并不需要原子性
,例如直接的读取或者写入,此时只需要保证顺序性
、可见性
即可。这时,则可以使用volatile
来实现较为轻量级的变量内存操作。
那volatile又是怎么实现了顺序性
、可见性
呢?其采用了一种称为内存屏障(Memory Barriers)
的技术。当对volatile类型的变量访问时,会操作对应的屏障指令
—— sfence、lfence、mfence(fullfence: sfence与lfence的结合),分别对应Load(读)和Store(写)这两种操作。在JMM中,分别将读和写这两个操作组合了四种可能场景:Load-Store、Store-Load、Load-Load、Store-Store。
对于屏蔽指令,在unsafe
中其实也有对应的API:
public native void loadFence();
public native void storeFence();
public native void fullFence();
为了保证volatile的顺序性
、可见性
, JVM会在volatile的访问前后插入对应的屏障指令,其伪代码如下:
//在写操作前,插入StoreStore,保证前面的写操作不会乱序;
//写操作后,插入StoreLoad,保证后面的读操作不会乱序(后面的读不会早于这个写操作进行读)
StoreStore `volatile Variable —— Write ` StoreLoad;
//在读操作后面,插入LoadLoad和LoadStore,保证后面的读、写操作都发生在后面,确保后面的读写都是最新的值
`volatile Variable —— read ` LoadLoad + LoadStore;
很明显,插入的内存屏障指令都是有代价的,而且代价要比普通指令高,特别是StoreLoad是四种指令里最高的,为此unsafe
对volatile变量的读写操作,进行了细化,从而在合适的场景可以采用代价更小的访问方式。以写操作为例,unsafe
提供了三种访问方式,分别是:
public native void putObject(Object var1, long var2, Object var4);
public native void putOrderedObject(Object var1, long var2, Object var4);
public native void putObjectVolatile(Object var1, long var2, Object var4);
其使用成本依次由低到高:
- putObject是普通写,避免了内存屏障的插入,其成本最小,但不保证
可见性
和顺序性
; - 其次是putOrderedObject,相对轻量,只保证了
顺序性
但不保证可见性
,只插入StoreStore
,减少了StoreLoad
这个最重量的屏障指令的插入。由于是缺少了StoreLoad
这个屏障,会使得写操作执行完,但其他线程不可见的情况(仍然读到老的值)。 - 最后是putObjectVolatile,其实现和直接对volatile变量赋值是一样的效果,但增加了灵活性,对于普通变量(非volatile修饰),也能有volatile的效果。相当于增加了一种手动插入内存屏障的方式。
putOrdered的理解
前面说了,putOrdered只保证了可见性
和顺序性
中的顺序性
,但并不保证可见性
,这个该怎么理解呢?
首先说顺序性
,这个容易理解,保证了指令的有序执行,确保不乱序,以满足我们对线性顺序
执行的要求。以单线程为例,顺序性
保证了指令的有序执行,若不能保证有序
,即使是单线程也会出现意料之外的结果,这个并不是我们希望的情况。
可以说满足了顺序性
,基本能解决大多数情况,但仍然不能满足happens-before
的关系,特别是在多线程的情况下。
这是由于,因为没有在写操作后面插入StoreLoad
这个屏障, 导致了其他线程(甚至是自己线程)无法及时看见变量值的变化,因此无法保证可见性
。但是由于CPU cache的MESI的缓存一致性的存在,可以确保在几个纳秒后保证一致,即类似最终一致性的效果。
所以在不需要强一致性的时候,即可见性
, 可以采用putOrdered方式来优化程序。
总结
本文通过对AtomicReferenceFieldUpdater作为引子,深入对unsafe中与内存读写有关的方法进行分析介绍,以及对volatile和内存屏障进行了简要的介绍。通过以上的介绍,可以了解AtomicReferenceFieldUpdater中的lazySet和putOrdered与普通set或putVolatile的区别。
原创不易,需要一点正反馈,点赞+收藏+关注,三连走一波~ ❤
如果这篇文章对您有所帮助,或者有所启发的话,请关注公众号【临虹路365号】(微信公众号ID:codegod365),您的支持是我们坚持写作最大的动力。