我已经阅读了一些关于多线程中 Java 数组元素可见性的问题和解答,但我仍然无法真正理解某些情况。为了演示我遇到的问题,我提出了一个简单的场景:假设我有一个简单的集合,通过将元素散列到一个存储桶中,将元素添加到其中一个存储桶中(存储桶就像某种列表) 。并且每个桶都是单独同步的。例如。 :
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]
下修改,您会说这段代码有效吗?我希望能够了解原因和原因,并更好地掌握可见性,谢谢。
最佳答案
第一个问题。
这种情况下的线程安全取决于对包含锁和存储桶(我们称之为容器)的对象的引用是否正确共享。
试想一下:一个线程正忙于实例化一个新的Container
对象(分配内存、实例化数组等),而另一个线程开始使用这个半实例化的对象,其中锁定
code> 和 buckets
仍然为 null(它们尚未被第一个线程实例化)。在本例中,此代码:
synchronized (locks[bucketNum]) {
变得损坏并抛出NullPointerException
。 final
关键字可以防止这种情况发生,并保证在对 Container
的引用不为 null 时,其 Final 字段已被初始化:
An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields. (JLS 17.5)
第二个问题。
假设locks
和buckets
字段是最终的并且,您不关心整个数组的一致性并且“每个bucket[i]只能在lock[i]下修改”,这段代码很好。
关于Java数组元素和内存可见性问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58026864/