【问题标题】:Using ` LinkedBlockingQueue` may cause null pointer exception使用 `LinkedBlockingQueue` 可能会导致空指针异常
【发布时间】:2020-08-30 10:49:19
【问题描述】:

我最近在学习java并发编程。我知道final 关键字可以保证安全发布。但是,当我阅读LinkedBlockingQueue源代码时,发现headlast字段没有使用final关键字。发现put方法中调用了enqueue方法,enqueue方法直接将值赋值给last.next。此时,last 可能是null,因为last 没有用final 声明。我的理解正确吗?虽然lock可以保证last读写线程安全,但是lock可以保证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();
    }
}

【问题讨论】:

  • 他们不能是final,因为他们已经改变了。创建一个变量final 并不能保证它永远不会为空。你的问题没有意义。

标签: java null final linkedblockingqueue


【解决方案1】:

根据这篇博文https://shipilev.net/blog/2014/safe-public-construction/,即使在构造函数中写入一个final 属性也足以实现安全初始化(因此您的对象将始终安全发布)。并且capacity 属性被声明为final

简而言之,我们在三种情况下发出尾随屏障:

最后一个字段被写入。请注意,我们并不关心实际写入的字段,我们在退出 (initializer) 方法之前无条件地发出屏障。这意味着如果您至少有一个 final 字段写入,则 final 字段语义会扩展到构造函数中编写的所有其他字段。

【讨论】:

    【解决方案2】:

    也许你错过了对 Java 的continuous assignment的理解

    //first last is inited in the constructor
    last = head = new Node<E>(null); // only the filed's value in last is null(item & next)
    
    // enqueue
    last = last.next = node;
    //equals:
    last.next = node;
    last = last.next;
    

    只有当你调用last.next 否则不会有NPE。

    【讨论】:

    • 我的意思是 last 可能为 null (last == null)
    • 我明白了,但是最后是在构造方法中初始化的。 last = head = new Node&lt;E&gt;(null); 所以 last 不会为空,只有里面的字段为空
    【解决方案3】:

    last 等于具有 null 值的 node 是正确的。然而这是故意的。 lock 只是为了确保每个线程都可以正确地执行此类中的修改。

    有时使用null 值是有意的,表示缺少值(在这种情况下为空队列)。因为变量是private,所以只能在类内部修改,所以只要写类的人知道null的可能性,一切都好。

    我认为您混淆了多个不一定相关的不同概念。请注意,因为lastprivate,所以没有发布。另外headlast是要修改的,所以不能是final

    编辑

    也许我误解了你的问题...

    null 永远不会直接分配给last。所以这可能发生的唯一地方是在构造函数中,在last 被分配new Node&lt;E&gt;(null) 之前。尽管我们可以确定构造函数在它被许多线程使用之前完成,但没有值的可见性保证。

    但是put 使用lock,这确实保证了使用中的可见性。所以如果没有使用lock,那么last实际上可能是null

    【讨论】:

    • 我的意思是 last 可能为 null (last == null)
    猜你喜欢
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 2014-08-02
    • 2015-09-12
    • 1970-01-01
    • 1970-01-01
    • 2021-11-25
    • 1970-01-01
    相关资源
    最近更新 更多