我已经阅读了有关多个线程对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]
下进行了修改,您会说这段代码有效吗?我希望能够了解为什么以及为什么不能,并更好地了解知名度,谢谢。
答案 0 :(得分:2)
第一个问题。
在这种情况下,线程安全性取决于是否正确共享对包含locks
和buckets
的对象的引用(我们称之为Container
)。
想象一下:一个线程正在忙于实例化一个新的Container
对象(分配内存,实例化数组等),而另一个线程开始使用这个半实例化的对象,其中locks
和{{1 }}仍然为空(它们尚未被第一个线程实例化)。在这种情况下,此代码:
buckets
被破坏并抛出 synchronized (locks[bucketNum]) {
。 NullPointerException
关键字可以防止这种情况,并保证到final
的引用不为null时,其最终字段已被初始化:
当一个对象被认为是完全初始化的 构造函数完成。只能看到引用的线程 该对象已完全初始化后的对象 查看该对象最终的正确初始化的值 领域。 (JLS 17.5)
第二个问题。
假设Container
和locks
字段是最终的和,您不必关心整个数组和“每个存储区[ i]只能在lock [i]“下修改,此代码可以。
答案 1 :(得分:0)
只需添加到Pavel的答案中即可:
在您问的第一个问题中
由于“存储桶”是最终的,因此即使没有同步也不会出现任何可见性问题。我的猜测是,有了同步,如果没有最终版本,这也不会出现任何可见性问题,这是正确的吗?
我不确定“可见性问题”是什么意思,但是可以肯定的是,如果没有多个线程访问synchronized
,而其中一个对其进行了修改,那么如果没有buckets[i]
,此代码将是不正确的(例如写信)。不能保证一个线程所写的内容对另一线程可见。这也涉及存储桶的内部结构,可以通过调用add
来修改。
请记住,final
上的buckets
仅与对数组本身的单一引用有关,与数组的单元无关。