Java数组元素和内存可见性问题

时间:2019-09-20 10:45:11

标签: java concurrency

我已经阅读了有关多个线程对Java数组元素的可见性的一些问题和解答,但是我仍然无法真正解决某些情况。为了演示我遇到的问题,我提出了一个简单的场景:假设我有一个简单的集合,该集合通过将元素散列为一个,将元素添加到其n个存储桶之一中(存储桶类似于某种列表) 。并且每个存储桶分别同步。例如。 :

private final Object[] locks = new Object[10];
private final Bucket[] buckets = new Bucket[10];

这里应该i保护水桶lock[i]。这是添加元素代码的样子:

public void add(Object element) {
        int bucketNum = calculateBucket(element); //hashes element into a bucket
        synchronized (locks[bucketNum]) {
            buckets[bucketNum].add(element);
        }
    }

由于“存储桶”是最终的,因此即使没有同步也不会出现任何可见性问题。我的猜测是,有了同步,如果没有最终版本,这也不会出现任何可见性问题,这是正确的吗?

最后,是比较棘手的部分。假设我想复制并合并所有存储桶的内容,并从任意线程清空整个数据结构,如下所示:

public List<Bucket> clear() {
    List<Bucket> allBuckets = new List<>();
    for(int bucketNum = 0; bucketNum < buckets.length; bucketNum++) {
        synchronized (locks[bucketNum]) {
            allBuckets.add(buckets[bucketNum]);
            buckets[bucketNum] = new Bucket();
        }    
    }
    return allBuckets;
}

我基本上将旧存储桶换成新创建的存储桶,然后返回旧存储桶。这种情况与add()的情况不同,因为我们没有修改数组中引用所引用的对象,而是直接更改了数组/引用。

请注意,当我持有存储桶1的锁时,我并不关心存储桶2是否被修改,我不需要结构完全同步且一致,仅可见性和接近一致性就足够了。

因此,假设每个bucket[i]仅在lock[i]下进行了修改,您会说这段代码有效吗?我希望能够了解为什么以及为什么不能,并更好地了解知名度,谢谢。

2 个答案:

答案 0 :(得分:2)

第一个问题。

在这种情况下,线程安全性取决于是否正确共享对包含locksbuckets的对象的引用(我们称之为Container)。

想象一下:一个线程正在忙于实例化一个新的Container对象(分配内存,实例化数组等),而另一个线程开始使用这个半实例化的对象,其中locks和{{1 }}仍然为空(它们尚未被第一个线程实例化)。在这种情况下,此代码:

buckets

被破坏并抛出 synchronized (locks[bucketNum]) { NullPointerException关键字可以防止这种情况,并保证到final的引用不为null时,其最终字段已被初始化:

  

当一个对象被认为是完全初始化的   构造函数完成。只能看到引用的线程   该对象已完全初始化后的对象   查看该对象最终的正确初始化的值   领域。 (JLS 17.5

第二个问题。

假设Containerlocks字段是最终的,您不必关心整个数组“每个存储区[ i]只能在lock [i]“下修改,此代码可以。

答案 1 :(得分:0)

只需添加到Pavel的答案中即可:

在您问的第一个问题中

  

由于“存储桶”是最终的,因此即使没有同步也不会出现任何可见性问题。我的猜测是,有了同步,如果没有最终版本,这也不会出现任何可见性问题,这是正确的吗?

我不确定“可见性问题”是什么意思,但是可以肯定的是,如果没有多个线程访问synchronized,而其中一个对其进行了修改,那么如果没有buckets[i],此代码将是不正确的(例如写信)。不能保证一个线程所写的内容对另一线程可见。这也涉及存储桶的内部结构,可以通过调用add来修改。

请记住,final上的buckets仅与对数组本身的单一引用有关,与数组的单元无关。