关于 putOrderedObject 系列的应用
lazySet是使用Unsafe.putOrderedObject方法,这个方法在对低延迟代码是很有用的,它能够实现非堵塞的写入,这些写入不会被Java的JIT重新排序指令(instruction reordering),这样它使用快速的存储-存储(store-store) barrier, 而不是较慢的存储-加载(store-load) barrier, 后者总是用在volatile的写操作上,这种性能提升是有代价的,虽然便宜,也就是写后结果并不会被其他线程看到,甚至是自己的线程,通常是几纳秒后被其他线程看到,这个时间比较短,所以代价可以忍受。
https://blog.csdn.net/mxy2002924/article/details/84803111
————————————————————————————————————————————————————————————————
ConcurrentLinkedQueue 是 Java 标准库提供的无锁队列,它里面就用到了这个黑科技。因为 Node 被构造出来后它得通过 cas 操作队尾 Node 的 next 引用接入链表,接入成功之后才需要被其它 CPU 看到,在 Node 刚构造出来的时候,Node 内的 item 实际不会被任何别的线程访问,所以看到 Node 的构造函数可以直接用 putObject 更新 item,等后续 cas 操作队列队尾 Node 的 next 时候再以 volatile 方式更新 next,从而带上 Barrier,更新完成后 next 的更新包括 Node 内 item 的更新就都被别的 CPU 看到了。从而减少操作 volatile 变量的开销。
// 实例化时时单线程的,结构的构建没有使用volatile 写
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
for (E e : c) {
checkNotNull(e);
Node<E> newNode = new Node<E>(e);
if (h == null)
h = t = newNode;
else {
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node<E>(null);
head = h;
tail = t;
}
/**
* When constructing a Node (before enqueuing it) we avoid paying for a volatile write to item by using Unsafe.putObject instead of a normal write.
* This allows the cost of enqueue to be "one-and-a-half" CASes.
**/
private static class Node<E> {
volatile E item;
volatile Node<E> next;
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) { // storestore 屏障
UNSAFE.putOrderedObject(this, nextOffset, val);
}
}
对于 unsafe.putOrderedInt() 的内容 Java 完全没给出解释,但从添加 lazySet()这个功能的地方: Bug ID: JDK-6275329 Add lazySet methods to atomic classes,能看出来其作用是在写入 volatile 状态前增加 StoreStore Barrier。它只保证本次写入不会重排到前面写入之前,但本次写入什么时候能刷写到内存是不做要求的,从而是一次轻量级的写入操作,在特定场景能优化性能。
https://juejin.cn/post/6844904144273145863
————————————————————————————————————————————————————————————————
假如使用场景对object的内存可见性并不敏感的话(不要求一个线程写入了object,object的新值立即对下一个读取的线程可见),在Intel 64/IA-32环境下,有更好的解决方案。根据上一章的内容,我们知道Intel 64/IA-32下写操作之间不会发生重排序,即在处理器中,构建SomeThing对象与赋值到object这两个操作之间的顺序性是可以保证的。这样看起来,仅仅使用volatile来避免重排序是多此一举的。但是,Java编译器却可能生成重排序后的指令。但令人高兴的是,Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操作重排序。而在Intel 64/IA-32架构下,StoreStore屏障并不需要,Java编译器会将StoreStore屏障去除。比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外,不会带来其它的性能开销。
https://tech.meituan.com/2014/09/23/java-memory-reordering.html
————————————————————————————————————————————————————————————————
ConcurrentHashMap大量使用Unsafe的putOrderedObject出于什么考虑?
这个方法的底层会调用UNSAFE.getObjectVolatile,这个方法的目的就是对于普通变量读取也能像volatile修饰的变量那样读取到最新的值,在前文中我们分析过,由于变量tab现在是一个普通的临时变量,如果直接调用tab[i]则大概率是拿不到最新的首节点的。细心的读者读到这里可能会想:Doug Lea是不是糊涂了,兜兜转换不是回到了原点么,为啥不刚开始就操作volatile变量呢,费了这老大劲。我们继续往下看。
在put方法的实现中,如果链表中没有key值相等的数据项,则会把新的数据项插入到链表头写入到数组中,其中调用的方法是:static final <K,V> void setEntryAt(HashEntry<K,V>[] tab, int i, HashEntry<K,V> e) {
UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e);
}
putOrderedObject这个接口写入的数据不会马上被其他线程获取到,而是在put方法最后调用unclock后才会对其他线程可见,参见前文中对JMM的描述:对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)这样的好处有两个,第一是性能,因为在持有锁的临界区不需要有同步主存的操作,因此持有锁的时间更短。第二是保证了数据的一致性,在put操作的finally语句执行完之前,put新增的数据是不对其他线程展示的,这是ConcurrentHashMap实现无锁读的关键原因。我们在这里稍微总结一下put方法里面最重要的三个细节,首先将volatile变量转为普通变量提升性能,因为在put中需要读取到最新的数据,因此接下来调用UNSAFE.getObjectVolatile获取到最新的头结点,但是通过调用UNSAFE.putOrderedObject让变量写入主存的时间延迟到put方法的结尾,一来缩小临界区提升性能,而来也能保证其他线程读取到的是完整数据。
https://www.zhihu.com/question/60888757/answer/1831937506
http://gee.cs.oswego.edu/dl/html/j9mm.html
浙公网安备 33010602011771号