是的,这是允许的。
主要公开在已经引用的部分:JMM
假设对象是“正确”构造的,那么构造对象后,分配给构造函数中最终字段的值将对所有其他线程可见,而无需同步。
正确构造对象意味着什么?它只是意味着在构造过程中不允许对正在构造的对象的引用“逃脱”。
换句话说,不要将对正在构造的对象的引用放在另一个线程可能能够看到它的任何位置;不要将其分配给静态字段,不要将其注册为任何其他对象的侦听器,依此类推。这些任务应在构造函数完成后完成,而不是在构造函数中完成**
*
所以,是的,在允许的范围内,这是可能的。最后一段充满了关于如何不做事的建议;每当有人说避免做X时,就隐含着X是可以做到的。
如果。。。 reflection
其他答案正确地指出了最终字段被其他线程正确看到的要求,例如构造函数末尾的冻结,链等。这些答案提供了对主要问题的更深入理解,应该首先阅读。本文重点介绍这些规则的可能例外。
重复次数最多的规则/短语可能是这里的这个,从尤金的答案中复制(顺便说一句,不应该有任何反对票):
当对象的构造函数完成时,将被视为已完全初始化。如果线程只能在对象完全初始化后才能看到对该对象的引用,则保证能够看到该对象的最终字段的正确 [赋值/加载/设置] 值。
请注意,我更改了术语“初始化”,并分配了、加载或设置了等效术语。这是有目的的,因为术语可能会误导我在这里的观点。
另一个恰当的说法是来自chrylis的那个 - 谨慎的乐观主义 - :
“最终冻结”发生在构造函数的末尾,从那时起,所有读取都保证准确。
JLS 17.5 最终字段语义指出:
如果线程只能在对象完全初始化后才能看到对该对象的引用,则保证可以看到该对象的最终字段的正确初始化值。
但是,你认为反思会对此给出一个f***吗?不,当然不是。它甚至没有读过那一段。
最终
字段的后续修改
这些陈述不仅是正确的,而且还得到了 .我不打算反驳它们,只是补充一些关于这个定律的例外的额外信息:反思。该机制,除其他外,可以在初始化后更改最终字段的值。JLS
字段的冻结发生在设置字段的构造函数的末尾,这是完全正确的。但是,冻结操作还有另一个未被考虑的触发器:通过反射初始化/修改字段时也会冻结字段(JLS 17.5.3):final
final
final
最终字段的冻结既发生在设置最终字段的构造函数的末尾,也发生在每次通过反射修改最终字段之后。
对字段的反射操作“打破”了规则:在构造函数正确完成之后,仍然不能保证字段的所有读取都是准确的。我会试着解释。final
final
让我们想象一下,所有正确的流程都已得到遵守,构造函数已初始化,并且线程可以正确查看实例中的所有字段。现在是时候通过反射对这些字段进行一些更改了(想象一下这是必需的,即使不寻常,我知道..)。final
遵循前面的规则,所有线程都等到所有字段都已更新:就像通常的构造函数方案一样,只有在冻结并且反射操作正确完成之后才能访问这些字段。这是违反法律的地方:
如果在字段声明中将最终字段初始化为常量表达式 (§15.28),则可能无法观察到对最终字段的更改,因为在编译时,该最终字段的使用将替换为常量表达式的值。
这很能说明问题:即使遵循了所有规则,如果该变量是基元或 String,并且您在字段声明中将其初始化为常量表达式,则您的代码也无法正确读取字段的赋值。为什么?因为该变量只是编译器的硬编码值,因此即使您的代码在运行时执行中正确更新了该值,编译器也不会再次检查该字段及其更改。final
因此,让我们对其进行测试:
public class FinalGuarantee
{
private final int i = 5;
private final long l;
public FinalGuarantee()
{
l = 1L;
}
public static void touch(FinalGuarantee f) throws Exception
{
Class<FinalGuarantee> rfkClass = FinalGuarantee.class;
Field field = rfkClass.getDeclaredField("i");
field.setAccessible(true);
field.set(f,555);
field = rfkClass.getDeclaredField("l");
field.setAccessible(true);
field.set(f,111L);
}
public static void main(String[] args) throws Exception
{
FinalGuarantee f = new FinalGuarantee();
System.out.println(f.i);
System.out.println(f.l);
touch(f);
System.out.println("-");
System.out.println(f.i);
System.out.println(f.l);
}
}
输出:
5
1
-
5
111
最终的 int 在运行时已正确更新,要对其进行检查,您可以调试和检查对象的字段值:i

两者都已正确更新。那么发生了什么,为什么仍然显示5?因为如 上所述,该字段在编译时直接替换为常量表达式的值,在本例中为 5。i
l
i
JLS
i
然后,即使遵循了所有以前的规则,对最终字段的每次后续读取都将不正确。编译器永远不会再次检查该字段:当您编写代码时,它不会访问任何实例的任何变量。它只会返回5:最后一个字段只是在编译时硬编码,如果在运行时对其进行更新,则任何线程都不会再次正确看到它。这违反了法律。i
f.i
作为在运行时正确更新字段的证明:

555
和 111L
都推送到堆栈中,字段获取其新分配的值。但是,在操纵它们时会发生什么,例如打印它们的价值?

16: before the update
42: after the update
没有字段访问权限,但只是一个“是的,肯定是5,返回它”。这意味着即使遵循了所有协议,也不能保证从外部线程正确看到字段。final
这会影响基元和字符串。我知道这是一个不寻常的情况,但它仍然是一个可能的情况。
其他一些有问题的场景(有些也与评论中引用的同步问题有关):
1-如果反射操作不正确,则在以下情况下,线程可能会陷入争用状态:synchronized
final boolean flag;
final int x;
1- Set flag to true
2- Set x to 100.
简化读者线程的代码:
while (!instance.flag)
Thread.sleep(1);
System.out.println(instance.x);
作为可能的情况,反射操作没有足够的时间来更新 ,因此可能会或不能正确读取字段。x
final
int x
2-在以下情况下,线程可能会陷入死锁:
final boolean flag;
1- Set flag to true
简化读者线程的代码:
while (!instance.flag) { }
我知道这不是最终字段的特定问题,而只是作为这些类型变量的错误读取流的可能情况添加的。最后两种情况只是不正确实现的结果,但想指出它们。