LinkedBlockingQueue如何保证多线程环境下head和last字段被安全初始化

阅读过《java并发编程实战》的人应该知道。由于StoreStore重排序和编译器的优化作用,构造函数中的赋值语句可能会被
重排序到被构造对象引用赋值之后。这样在多线程环境下,对象被发布出去,可能得到的是一个部分初始化或者未初始化的
对象。通过给字段添加final关键字,JMM可以保证字段不被重排序到被构造对象引用赋值之后。那么现在问题来了,在阅读
LinkedBlockingQueue源码的时候,我发现head和last字段没有被final修饰(虽然说这两个字段后期需要修改不能使用final,
但是这里先讨论初始化的问题)。这里用last字段举例,我发现enqueue方法在put方法中被调用,并且enqueue方法直接将
值分配给last.next。 此时,last可能为null,因为未使用final声明last。尽管锁可以保证last读写线程的安全性,但是我觉得不能保
证last个是正确的初始值而不是null

public class LinkedBlockingQueue<E> extends AbstractQueue<E>

implements BlockingQueue<E>, java.io.Serializable {

transient Node<E> head;

private transient Node<E> last;

public LinkedBlockingQueue(int capacity) {

if (capacity <= 0) throw new IllegalArgumentException();

this.capacity = capacity;

last = head = new Node<E>(null);

}

private void enqueue(Node<E> node) {

// assert putLock.isHeldByCurrentThread();

// assert last.next == null;

last = last.next = node;

}

public void put(E e) throws InterruptedException {

if (e == null) throw new NullPointerException();

// Note: convention in all put/take/etc is to preset local var

// holding count negative to indicate failure unless set.

int c = -1;

Node<E> node = new Node<E>(e);

final ReentrantLock putLock = this.putLock;

final AtomicInteger count = this.count;

putLock.lockInterruptibly();

try {

/*

* Note that count is used in wait guard even though it is

* not protected by lock. This works because count can

* only decrease at this point (all other puts are shut

* out by lock), and we (or some other waiting put) are

* signalled if it ever changes from capacity. Similarly

* for all other uses of count in other wait guards.

*/

while (count.get() == capacity) {

notFull.await();

}

enqueue(node);

c = count.getAndIncrement();

if (c + 1 < capacity)

notFull.signal();

} finally {

putLock.unlock();

}

if (c == 0)

signalNotEmpty();

}

}

------分割线---------------增加两个我可能认为是证据的代码----
3OEI__O(2OFJ{{W9LZA`6QX.png
为了保证可见性,构造函数特加了锁

RZL9}3FN8~6}R6OWT1VEIR4.png
这个例子发volatile变量State赋值放到最后也是为了保证可见性。因为callable没有任何修饰,既不是final也不是volatile

----------------------分割。我想我找到了合理的解释--------------

参考这篇文章:https://shipilev.net/blog/2014/safe-public-construction/

一个final就够了

-------------分割-------------------
可能我没讲清楚,我看的源码时 jit 服务端c2编译器的优化手段,非热点代码是不会被优化的【所以跑一次的那种是看不出来的】。其次在x86下没有StoreStore重排序,所以x86平台是没有任何操作的。至于伪代码。可以看我在下面的评论。
我的结论:LinkedBlockingQueue需要自己保证安全初始化!

回答:

谢谢各位的回答。我想我在jdk源码中找到了我满意的答案:
http://hg.openjdk.java.net/jd...

image.png
java内存语义要求需要所有字段都为final才能安全初始化,不过jvm的实现取了巧。只要存在final写,就会在构造函数返回前,所有写之后放置内存屏障,就能保证所有字段安全初始化。
我看的源码时 jit 服务端c2编译器的优化手段,非热点代码是不会被优化的【所以跑一次的那种是看不出来的】。其次在x86下没有StoreStore重排序,所以x86平台是没有任何操作的。至于伪代码。可以看我在下面的评论。
我的结论:LinkedBlockingQueue需要自己保证安全初始化!

回答:

putLock不是吗?

以上是 LinkedBlockingQueue如何保证多线程环境下head和last字段被安全初始化 的全部内容, 来源链接: utcz.com/p/170829.html

回到顶部