• 3
  • 新人请关照

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需要自己保证安全初始化!

阅读 463
评论
    2 个回答
    • 3
    • 新人请关照

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

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

      • 488

      putLock不是吗?

        撰写回答

        登录后参与交流、获取后续更新提醒