最终字段对于线程安全是否真的有用?

我已经每天使用Java Memory Model工作多年了。 我认为我对数据竞争的概念以及避免它们的不同方法(例如,同步块,易变变量等)有很好的理解。 但是,我仍然认为我完全不了解内存模型,这是最终的类字段在没有任何进一步同步的情况下应该是线程安全的方式。

所以根据规范,如果一个对象被正确初始化(也就是说,没有引用对象在其构造函数中以某种方式转义,使得引用可以被另一个线程看到),那么,在构造之后,任何看到该对象的线程对象将保证看到对象的所有最终字段的引用(在它们构造时的状态),没有任何进一步的同步。

特别是,标准( http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4 )说:

final字段的用法模型很简单:在该对象的构造函数中设置对象的最终字段; 并且在对象的构造函数完成之前,不要在另一个线程可以看到的地方写入对正在构造的对象的引用。 如果遵循此操作,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。 它还将看到那些最终字段引用的任何对象或数组的版本,这些字段至少与最终字段一样是最新的。

他们甚至给出了以下示例:

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 = fx; // guaranteed to see 3 int j = fy; // could see 0 } } } 

其中线程A应该运行“reader()”,并且线程B应该运行“writer()”。

到目前为止,显然是如此的好。

我主要担心的是……这在实践中真的有用吗? 据我所知,为了使线程A(运行“reader()”)看到对“f”的引用,我们必须使用一些同步机制,例如使f volatile,或者使用lock来同步访问F。 如果我们不这样做,我们甚至不能保证“reader()”能够看到初始化的“f”,也就是说,由于我们没有同步访问“f”,读者可能会看到“ null“而不是由编写器线程构造的对象。 这个问题在http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong中说明 ,这是Java内存模型的主要参考之一[大胆强调我的]:

现在,说完所有这些,如果在一个线程构造一个不可变对象(即一个只包含最终字段的对象)之后,你想确保所有其他线程都能正确看到它,你通常还需要使用同步。 例如,没有其他方法可以确保第二个线程可以看到对不可变对象的引用 。 程序从最终字段获得的保证应该仔细调整,并深入细致地了解如何在代码中管理并发性。

因此,如果我们甚至不能保证看到对“f”的引用,那么我们必须使用典型的同步机制(易失性,锁定等),并且这些机制确实已经导致数据竞争消失,最终的需求是我甚至都不会考虑的事情。 我的意思是,如果为了使“f”对其他线程可见,我们仍然需要使用volatile或synchronized块,并且它们已经使内部字段对其他线程可见……有什么意义(在线程安全术语中)首先在场上进行决赛?

我认为您误解了JLS示例的目的:

 static void reader() { if (f != null) { int i = fx; // guaranteed to see 3 int j = fy; // could see 0 } } 

此代码不保证调用reader()的线程将看到f的最新值。 但它所说的是,如果你确实看到f为非null,那么fx保证为3 ……尽管事实上我们实际上没有做任何明确的同步。

那么构造函数中的终结隐式同步是否有用? 当然是…… IMO。 这意味着每次访问不可变对象的状态时,我们不需要进行任何额外的同步。 这是一件好事,因为同步通常需要缓存读取或直写,这会降低程序的速度。

但Pugh所说的是,您通常需要同步以获得对不可变对象的引用。 他指出,使用不可变对象(使用final实现)并不能免除同步的需要……或者需要理解应用程序的并发/同步实现。


问题是我们仍然需要确保读者会得到一个非空的“f”,而这只有在我们使用其他同步机制时才有可能,这种机制已经提供了允许我们为fx看到3的语义而且如果这是案例,为什么要使用final作为线程安全的东西呢?

同步获取引用和同步以使用引用之间存在差异。 第一个我可能只需做一次。 第二个我可能需要做很多次…同样的参考。 即使它是一对一的,我仍然减少了同步操作的数量……如果我(假设)将不可变对象实现为线程安全的。

TL; DR:大多数软件开发人员应该忽略有关Java内存模型中 最终变量的特殊规则。 它们应遵循一般规则:如果程序没有数据争用 ,则所有执行似乎都是顺序一致的 。 在大多数情况下, 最终变量不能用于提高并发代码的性能,因为Java内存模型中的特殊规则会为最终变量创建一些额外的成本,这使得volatile在几乎所有用例中都优于final变量。

关于最终变量的特殊规则在某些情况下会阻止最终变量显示不同的值。 但是,在性能方面,规则无关紧要。


话虽如此,这里有一个更详细的答案。 但我必须警告你。 以下描述可能包含一些不稳定的信息,大多数软件开发人员都不应该关心这些信息,如果他们不了解它,那就更好了。

关于Java内存模型中的最终变量的特殊规则在某种程度上意味着,如果成员变量是最终成员或者不是最终成员变量,它会对Java VM和Java JIT编译器产生影响。

 public class Int { public /* final */ int value; public Int(int value) { this.value = value; } } 

如果您查看Hotspot源代码,您将看到编译器检查类的构造函数是否写入至少一个最终变量。 如果它这样做,编译器将为构造函数发出额外的代码,更确切地说是内存释放障碍 。 您还可以在源代码中找到以下注释:

这个方法(必须是Java规则的构造函数)写了一个final。 在构造函数发布对新构造函数对象的引用之后,必须在所有代码之前将所有初始化的效果提交到内存。 我们不是在等待发布,而是在这里阻止写入。 我们强制完成所有写操作,而不是仅对那些需要完成的写操作设置障碍。

这意味着最终变量的初始化类似于写入volatile变量。 它意味着某种内存释放障碍 。 但是,从引用的评论中可以看出, 最终变量可能更加昂贵。 更糟糕的是,无论是否在并发代码中使用, 最终变量都会产生这些额外成本。

这很糟糕,因为我们希望软件开发人员使用最终变量来提高源代码的可读性可维护性 。 不幸的是,使用最终变量会显着影响程序的性能。


问题仍然存在:是否有任何使用案例,关于最终变量的特殊规则有助于提高并发代码的性能?

这很难说,因为它取决于Java VM的实际实现和机器的内存架构。 到目前为止,我还没有看到任何此类用例。 快速浏览java.util.concurrent包的源代码也没有透露任何信息。

问题是: 最终变量的初始化与写入易失性primefaces变量一样昂贵。 如果使用volatile变量作为新创建对象的引用,则会获得与exception相同的行为和成本,即引用也将立即发布 。 因此,使用最终变量进行并发编程基本上没有任何好处。

你是对的,因为锁定提供了更强的保证,对于final s的可用性的保证在存在锁定时并不特别有用。 但是,并不总是需要锁定才能确保可靠的并发访问。

据我所知,为了使线程A(运行“reader()”)看到对“f”的引用,我们必须使用一些同步机制,例如使f volatile,或者使用lock来同步访问F。

使f volatile不是同步机制; 它会在每次访问变量时强制线程读取内存,但不会同步访问内存位置。 锁定是一种同步访问的方法,但实际上并不需要保证两个线程可靠地共享数据。 例如,您可以使用ConcurrentLinkedQueue类(它是一个无锁的并发集合* )将数据从读取器线程传递到编写器线程,并避免同步。 您还可以使用AtomicReference来确保对对象的可靠并发访问而不进行锁定。

当您使用无锁并发时 ,对final字段可见性的保证会派上用场。 如果您创建一个无锁集合,并使用它来存储不可变对象,您的线程将能够访问对象的内容而无需额外的锁定。

* ConcurrentLinkedQueue不仅是无锁的,而且是一个无等待的集合(即无锁集合,其附加保证与此讨论无关)。

是最终的最终字段在线程安全方面很有用。 它可能在您的示例中没有用,但是如果您查看旧的ConcurrentHashMap实现,get方法在搜索值时不会应用任何锁定,尽管存在查找正在查找的风险时列表可能会更改(想到ConcurrentModificationException)。 但是,CHM使用最终提交的“下一个”字段列表,保证列表的一致性(前面/尚未看到的项目不会增长或缩小)。 因此优点是线程安全是在没有同步的情况下建立的。

从文章

利用不变性

通过使Entry元素几乎不可变来避免一个重要的不一致来源 – 所有字段都是最终的,除了值字段,它是易失性的。 这意味着元素不能添加到哈希链的中间或末尾或从中删除 – 元素只能在开头添加,删除包括克隆全部或部分链并更新列表头指针。 因此,一旦你有一个哈希链的引用,虽然你可能不知道你是否有对列表头部的引用,但你知道列表的其余部分不会改变它的结构。 此外,由于值字段是易失性的,您将能够立即看到值字段的更新,从而大大简化了编写可以处理可能过时的内存视图的Map实现的过程。

虽然新的JMM为最终变量提供初始化安全性,但旧的JMM没有,这意味着另一个线程可以看到最终字段的默认值,而不是对象的构造函数放置的值。 实现必须准备好检测这一点,它通过确保Entry的每个字段的默认值不是有效值来实现。 构造列表使得如果任何Entry字段看起来具有其默认值(零或null),则搜索将失败,从而提示get()实现再次同步和遍历链。

文章链接: https : //www.ibm.com/developerworks/library/j-jtp08223/