我已经每天使用Java Memory Model工作多年了。我认为我对数据竞争的概念以及避免它们的不同方法(例如,同步块,易变变量等)有很好的理解。但是,我仍然认为我完全不了解内存模型,这是最终类的字段应该是线程安全的,而不需要任何进一步的同步。
所以根据规范,如果一个对象被正确初始化(也就是说,没有引用该对象在其构造函数中以某种方式转义,以便另一个线程可以看到引用),那么,在构造之后,任何线程看到对象将保证看到对象的所有最终字段的引用(在它们构造时的状态),没有任何进一步的同步。
特别是,标准(http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4)说:
最终字段的使用模型很简单:设置最终字段 对于该对象的构造函数中的对象;并且不要写 引用正在另一个地方构建的对象 线程可以在对象的构造函数完成之前看到它。如果这 跟随,然后当另一个线程看到该对象时,那 线程将始终看到正确构造的版本 对象的最终字段。它还会看到任何对象的版本或 由最终字段引用的数组,这些字段至少是最新的 最后的字段是。
他们甚至给出了以下示例:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
其中线程A应该运行" reader()",并且线程B应该运行" writer()"。
到目前为止,显然很好。
我主要担心的是......这在实践中真的有用吗?据我所知,为了使线程A(运行" reader()")看到对" f"的引用,我们必须使用一些同步机制,例如使f易失,或使用锁来同步对f的访问。如果我们不这样做,我们甚至不能保证"读者()"将能够看到一个初始化的" f",也就是说,由于我们没有同步访问" f",读者可能会看到" null"而不是由编写者线程构造的对象。这个问题在http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong中说明,它是Java内存模型的主要参考之一[大胆强调我的]:
现在,已经说过所有这些,如果,在一个线程构造之后 不可变对象(即只包含最终字段的对象), 你想确保所有其他人都能正确看到它 线程,你仍然通常需要使用同步。 没有 以其他方式确保,例如,对不可变的引用 第二个线程将看到对象。保证计划 从最后的领域获得应该仔细调整与深刻和 仔细了解代码中如何管理并发。
因此,如果我们甚至不能保证看到对" f"的引用,那么我们必须使用典型的同步机制(易失性,锁定等),这些机制确实已经导致数据竞争走开,对决赛的需求是我甚至不会考虑的。我的意思是,如果是为了制造" f"对于其他线程可见,我们仍然需要使用volatile或synchronized块,并且它们已经使内部字段对其他线程可见...在第一个字段中使字段成为最终的点(在线程安全术语中)是什么放置?
答案 0 :(得分:10)
我认为您误解了JLS示例的目的:
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
此代码不保证调用f
的线程会看到reader()
的最新值。但它的含义是,如果你确实看到f
为非空,那么f.x
保证为3
......尽管事实上我们实际上并没有显式同步。
这对于构造函数中的终结的隐式同步是否有用?当然是...... IMO。这意味着每次访问不可变对象的状态时,我们都不需要需要进行任何额外的同步。这是一件好事,因为同步通常需要缓存读取或直写,这会降低程序的速度。
但Pugh所说的是,通常需要首先同步获取对不可变对象的引用。他指出,使用不可变对象(使用final
实现)并不能免除同步的需要......或者需要理解应用程序的并发/同步实现。
问题在于我们仍然需要确保读者会得到一个非空的“f”,而这只有在我们使用其他同步机制时才有可能,这种机制已经提供了允许我们为f.x看到3的语义。如果是这样的话,为什么还要使用final作为线程安全的东西呢?
同步获取引用和同步以使用引用之间存在差异。第一个我可能只需做一次。第二个我可能需要做很多次...同样的参考。即使它是一对一的,我仍然减少了同步操作的数量......如果我(假设)将不可变对象实现为线程安全的。
答案 1 :(得分:7)
TL; DR:大多数软件开发人员应该忽略 Java内存模型中有关最终变量的特殊规则。它们应遵循一般规则:如果程序没有数据竞争,则所有执行似乎都是顺序一致。在大多数情况下, final 变量不能用于提高并发代码的性能,因为 Java内存模型中的特殊规则会为 final创建一些额外的成本变量,几乎所有用例的 volatile 优于 final 变量的原因。
关于 final 变量的特殊规则在某些情况下会阻止 final 变量显示不同的值。但是,在性能方面,规则无关紧要。
话虽如此,这里有一个更详细的答案。但我必须警告你。以下描述可能包含一些不稳定的信息,大多数软件开发人员都不应该关心这些信息,如果他们不了解它,那就更好了。
Java Memory Model 中关于 final 变量的特殊规则暗示,如果成员变量是,那么它对Java VM和Java JIT编译器有所不同 final 或者如果不是。
public class Int {
public /* final */ int value;
public Int(int value) {
this.value = value;
}
}
如果您查看 Hotspot 源代码,您将看到编译器检查类的构造函数是否至少写入一个 final 变量。如果它这样做,编译器将为构造函数发出额外的代码,更确切地说是内存释放障碍。您还可以在源代码中找到以下注释:
此方法(必须是Java规则的构造函数) 写了一个决赛。所有初始化的效果必须是 在构造函数之后的任何代码之前提交内存 发布对新构造函数对象的引用。 我们不是等待发布,而是阻止发布 写到这里。而不是只对那些写入设置障碍 这些都需要完成,我们强制完成所有写入。
这意味着 final 变量的初始化类似于写入 volatile 变量。它意味着某种内存释放障碍。但是,从引用的评论中可以看出, final 变量可能更加昂贵。更糟糕的是,无论是否在并发代码中使用它们, final 变量都会产生这些额外成本。
这很糟糕,因为我们希望软件开发人员使用 final 变量来提高源代码的可读性和可维护性。不幸的是,使用 final 变量会显着影响程序的性能。
问题仍然存在:是否存在关于 final 变量的特殊规则有助于提高并发代码性能的用例?
这很难说,因为它取决于Java VM的实际实现和机器的内存架构。到目前为止,我还没有看到任何此类用例。快速浏览包 java.util.concurrent 的源代码也没有透露任何内容。
问题是: final 变量的初始化与写入 volatile 或 atomic 变量一样昂贵。如果您使用 volatile 变量来引用新创建的对象,则会获得相同的行为和成本(例外),该引用也将立即发布。因此,使用 final 变量进行并发编程基本上没有任何好处。
答案 2 :(得分:5)
你是对的,因为锁定提供了更强的保证,final
s的可用性保证在存在锁定时并不特别有用。但是,并不总是需要锁定才能确保可靠的并发访问。
据我所知,为了使线程A(运行“reader()”)看到对“f”的引用,我们必须使用一些同步机制,例如使f volatile,或使用lock同步访问f。
使f
volatile不是同步机制;它会强制线程在每次访问变量时读取内存,但不会同步访问内存位置。锁定是一种同步访问的方法,但实际上并不需要保证两个线程可靠地共享数据。例如,您可以使用ConcurrentLinkedQueue<E>
类(一个无锁的并发集合 * )将数据从读取器线程传递到写入器线程,并避免同步。您还可以使用AtomicReference<T>
来确保在不锁定的情况下可靠地并发访问对象。
当您使用lock-free concurrency时,对final
字段可见性的保证会派上用场。如果你创建一个无锁集合,并使用它来存储不可变对象,你的线程将能够访问对象的内容而无需额外的锁定。
* ConcurrentLinkedQueue<E>
不仅是无锁的,而且是一个无等待的集合(即无锁集合,其附加保证与此讨论无关)。
答案 3 :(得分:1)
是最终的最终字段在线程安全方面很有用。它可能在您的示例中没有用,但是如果您查看旧的ConcurrentHashMap
实现,get方法在搜索值时不会应用任何锁定,尽管存在查找正在查找的风险列表可能会更改(想想ConcurrentModificationException)。然而,CHM使用最终提交的“下一个”字段列表,保证列表的一致性(前面/尚未看到的项目不会增长或缩小)。因此优点是线程安全是在没有同步的情况下建立的。
来自文章
利用不变性
通过输入可以避免一个重要的不一致来源 元素几乎不可变 - 所有字段都是最终的,除了 值字段,它是易失性的。这意味着元素不可能 添加到哈希链的中间或末尾或从中删除 - 元素只能在开头添加,并且删除涉及 克隆全部或部分链并更新列表头指针。 所以,一旦你有一个哈希链的引用,你可能不知道 无论你是否有对列表头部的引用,你都知道 列表的其余部分不会改变其结构。另外,自从 值字段是易失性的,您将能够看到值的更新 现场立即,大大简化了编写Map的过程 可以处理可能陈旧的内存视图的实现。
虽然新的JMM为最终变量提供初始化安全性, 旧的JMM没有,这意味着另一个可能 线程查看最终字段的默认值,而不是 对象的构造函数放置在那里的值。实施 必须准备好检测这一点,它通过确保来做到这一点 Entry的每个字段的默认值不是有效值。 构造列表使得如果出现任何Entry字段 有默认值(零或null),搜索将失败, 提示get()实现同步和遍历 再次链。