【发布时间】:2014-04-29 04:44:58
【问题描述】:
许多问题/答案表明,如果一个类对象有一个final 字段,并且在构造过程中没有对它的引用暴露给任何其他线程,那么一旦构造函数,所有线程都可以保证看到写入该字段的值完成。他们还指出,将对从未被外部线程访问过的可变对象的引用存储到final 字段中将确保在存储之前对该对象进行的所有突变在所有访问的线程上都是可见的对象通过字段。不幸的是,这两种保证都不适用于非final 字段的写入。
然而,一个我没有看到答案的问题是:如果一个类的语义使得一个字段不能是final,但希望确保该字段和由此识别的对象的“发布” ,最有效的方法是什么?例如,考虑
class ShareableDataHolder<T>
{
Object data; // Always identifies either a T or a SharedDataHolder<T>
}
private class SharedDataHolder<T> extends ShareableDataHolder<T>
{
Object data; // Always identifies either a T or a lower-numbered SharedDataHolder<T>
final long seq; // Immutable; necessarily unique
}
意图是data 最初将直接标识一个数据对象,但它可以在任何时候合法地更改为标识一个直接或间接封装等效数据对象的SharedDataHolder<T>。如果对data 的任何读取可能会任意返回曾经写入data 的任何值,但如果读取null 则可能会失败,则假设所有代码都已写入正常工作(尽管不一定以最佳效率)。
声明volatile Object data 在语义上是正确的,但可能会对随后对该字段的每次访问产生额外成本。在最初设置字段后输入虚拟锁会起作用,但会不必要地慢。有一个虚拟的final 字段,对象设置它来标识自己似乎应该可以工作;尽管从技术上讲,我认为可能需要通过另一个字段完成对另一个字段的所有访问,但我看不到任何重要的现实场景。在任何情况下,拥有一个目的只是通过其存在提供适当同步的虚拟字段似乎是一种浪费。
是否有任何干净的方法来通知编译器在构造函数中对data 的特定写入应该与在构造函数返回后发生的对该字段的任何读取具有发生之前的关系(就像这种情况一样如果字段为final),而无需支付与volatile、锁等相关的费用?或者,如果一个线程要读取data 并发现它为空,它是否可以以某种方式重复读取以建立关于data 写入的“发生之后”[认识到这样的请求可能很慢,但不需要经常发生]?
PS--如果happens-before关系是不可传递的,那么在以下场景中是否存在适当的happens-before关系?
- 线程 1 写入某个对象
Fred中的非最终字段dat,并将对它的引用存储到最终字段George。 - 线程 2 将引用从
George复制到非最终字段Larry。 - 线程 3 读取
Larry.dat。
据我所知,Fred 的字段dat 的写入和George 的读取之间存在发生前的关系。 Fred 的dat 的写入和Larry 的读取之间是否存在happens-before 关系,后者返回从final 引用复制到Fred 的对Fred 的引用?如果没有,是否有任何“安全”的方法可以将 final 字段中包含的引用复制到可通过其他线程访问的非最终字段?
PPS--如果在主构造函数完成之前从未在其创建线程之外访问对象及其组成部分,并且主构造函数的最后一步是在主对象中存储一个 final 对其自身的引用,是否存在任何“合理的”实现/场景,其中另一个线程可以看到部分构造的对象,无论是否有任何东西实际使用该 final 引用?
【问题讨论】:
-
" 他们还指出,在最终字段中存储对外部线程从未访问过的可变对象的引用将确保在存储之前对该对象进行的所有更改在通过该字段访问对象的所有线程上可见"
-
另外:“声明 volatile 对象数据在语义上是正确的,但可能会在每次后续访问该字段时产生额外成本。” volatile 字段贵。 More info
-
问题是两个不相关的特性——不可重新分配,可见性——被同一个关键字
final不必要地耦合。
标签: java thread-safety immutability