我有一个关于
Java内存模型的问题.这是一个简单的课堂介绍问题:
public class ImmutableIntArray { private final int[] array; public ImmutableIntArray() { array = new int[10]; for (int i = 0; i < 10; i++) { array[i] = i; } } // Will always return the correct value? public int get(int index) { return array[index]; } }
据我所知,JMM保证最终字段的值在构建后对其他线程是可见的.但是我想确保其他线程将在构建后看到存储在数组中的最新版本的数据.
当然上面的代码只是一个简单的例子,提出了这个问题,实际上我想为直接字节缓冲区实现一个简单的缓存,我不想依赖一些Collection类.目前我正在使用ReentrantReadWriteLock来确保正确的行为,但如果可以的话我想避免这种情况.
解决方法
在这个例子中,一切都会很好(嗯,让我们暂停判断一下).不可变性在线程安全性方面是一个不利的因素 – 如果一个值不能改变,大多数并发问题就不再是一个问题.
Amir提到的volatile通常是有用的 – 但是构造函数也具有类似的用于确保可见性的最终变量的语义.有关详细信息,请参阅JLS clause 17.5 – 本质上,构造函数形成写入最终变量和随后的任何读取之间的发生关系.
编辑:所以你在构造函数中设置数组的引用,它在该点上的所有线程都可见,然后它不会改变.所以我们知道所有其他线程将看到相同的数组.但是数组的内容呢?
就像现在这样,数组元素在波动性方面没有任何特殊的语义,就像你刚刚宣布一个类似于:
public class ArrayTen { private int _0; private int _1; // ... private int _9; public int get(int index) { if (index == 0) return _0; // etc. } }
所以 – 另一个线程只会看到这些变量,如果我们可以做一些事情来建立事件发生的关系.如果我的理解正确,则需要对原始代码进行一些小的更改.
我们已经知道数组引用的设置发生在构造函数结束之前.一个永远是真的附加点就是一个线程中的动作发生在同一个线程之后的动作中.所以我们可以通过首先设置数组字段,然后分配最后一个字段来组合它们,以获得可见性的这种传递性保证.这当然需要一个临时变量:
public class ImmutableIntArray { private final int[] array; public ImmutableIntArray() { int[] tmp = new int[10]; for (int i = 0; i < 10; i++) { tmp[i] = i; } array = tmp; } // get() etc. }
我认为这是安全的,现在我们已经改变了看似无关紧要的任务和人口的顺序.
但是,再次,我可能还有其他一些错误,这意味着并发保证不如希望那么强壮.这个问题在我心中是一个很好的例子,为什么要编写防弹多线程代码是棘手的,即使你认为你在做一些非常简单的事情,以及它需要大量的思考和谨慎(然后是错误修正)才能正确.