ArrayBlckingQueue构造函数加锁问题


ArrayBlckingQueue构造方法加锁问题

构造方法加锁

// 该方法为ArrayBlckingQueue的其中一个构造方法
// 源码作者在line15加了一行注释:加锁仅为了保证可见性,不是为了互斥。
// 构造方法数功能描述如下,比较容易理解,不再解释
/**
  * Creates an {@code ArrayBlockingQueue} with the given (fixed)
  * capacity, the specified access policy and initially containing the
  * elements of the given collection,
  * added in traversal order of the collection's iterator.
  */
public ArrayBlockingQueue(int capacity, boolean fair,
                          Collection<? extends E> c) {
    this(capacity, fair);
    final ReentrantLock lock = this.lock;
    lock.lock(); // Lock only for visibility, not mutual exclusion
    try {
        int i = 0;
        try {
            for (E e : c) {
                checkNotNull(e);
                items[i++] = e;
            }
        } catch (ArrayIndexOutOfBoundsException ex) {
            throw new IllegalArgumentException();
        }
        count = i;
        putIndex = (i == capacity) ? 0 : i;
    } finally {
        lock.unlock();
    }
}
// 构造函数中涉及到了对成员变量 items[] count putIndex的初始化
// 以下为这几个成员变量在类中的声明
/** The queued items */
final Object[] items;

/** items index for next put, offer, or add */
int putIndex;

/** Number of elements in the queue */
int count;

问题分析

为什么在构造中需要对代码加锁,并且为什么仅是为了保证可见性?

  1. 数组变量items是用final修饰的。

在一个对象的构造过程中,如果构造器没有出现外部指针引用当前对象的情况,即没有出现引用逃逸,那么final修饰的变量在在构造方法中被初始化时,JVM会保证其他线程对final修饰变量的可见性。

即在不出现引用逃逸的情况下,final会保证在构造器对final变量的初0始化不会出现指令重排,并且在构造器完成后,其他线程访问final修饰的变量时,能够看到构造器完成时final修饰变量的最新值。也就是说在构造器中对final变量的初始化是线程安全的,但final变量指向对象中的值如果没有被final修饰,不保证它们的可见性,比如代码line20对数组元素赋值的操作。

这里列出部分规范和解释,详情可以去链接或者看我之前文章[Java并发-Happens-Before规则-final关键字]

Set the final fields for an object in that object’s constructor; and do not write a reference to the object being constructed in a place where another thread can see it before the object’s constructor is finished. If this is followed, then when the object is seen by another thread, that thread will always see the correctly constructed version of that object’s final fields. It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

[Java Language Specification-Chapter 17. Threads and Locks-17.5]

What does it mean for an object to be properly constructed? It simply means that no reference to the object being constructed is allowed to “escape” during construction. (See Safe Construction Techniques for examples.) In other words, do not place a reference to the object being constructed anywhere where another thread might be able to see it; do not assign it to a static field, do not register it as a listener with any other object, and so on. These tasks should be done after the constructor completes, not in the constructor.

**you can have a final pointer to an array and not have to worry about other threads seeing the correct values for the array reference, but incorrect values for the contents of the array. **

How do final fields work under the new JMM?

Presence of final guarantees that other threads would see values in the map after constructor finished without any external synchronization. Without final it cannot be guaranteed in all cases.

Java concurrency: is final field (initialized in constructor) thread-safe?

根据第一点的分析结论,可以得出:

代码中为了保证对items写操作的可见性,又因为finalvolatile不能并列修饰,所以需要加锁

  1. 对于putIndexcount,可能会出现指令重排现象,也需要保证可见性。[Java并发-Happens-Before规则-final关键字]

    首先这两个字段不能用final来保证可见性,因为这两个字段的值还要变化。

    volatile确实也可以保证可见性,但是putIndexcount在本类的其他方法的代码中,会频繁地在加锁的情况下进行读写。

    加锁本身已经保证了putIndexcount的可见性,如果再加上volatile,会导致这两个变量频繁地从内存中读写,反而会降低方法的效率。

结合第1点和第2点:items必须要用锁来保证可见性,putIndexcount用锁保证可见性效率更好。

因此,这个构造方法中需要对涉及到items的写,putIndexcount初始化的代码加锁。


附上我在StackOverFlow的一个相关提问:[Why ArrayBlockingQueue constructor use ReentrantLock for visibility?]

The lock guarantees the visibility of all writes during: to count, to putIndex, and to the elements of items that it changes.

It doesn’t need to guarantee mutual exclusion, as it is in the constructor and since the reference to this hasn’t been given to other threads, there is no need for mutual exclusion (but it would guarantee that as well if the reference to this was given out before that point)

The comment is merely saying that the purpose of the lock is the visibility effects.

As to why you can’t use volatile:

The methods that retrieve values from the queue, like poll, take and peek do need to lock for mutual exclusion. Making a variable volatile is not necessary; it could have an adverse performance impact.

It would also be hard to get it right because of the ordering: a volatile read happens before (JLS terminology) a volatile write on the same variable. That means that the constructor would have to write to the volatile variable as its last action, while all code that needs to be correctly synchronized needs to read that volatile variable first before doing anything else.

Locks are much easier to reason about and to get the ordering of accesses right, and, in this case - they are required in any case to execute multiple writes as one atomic action.

—— Erwin Bolwidt


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录