Java 不可变对象是否对不正确的发布免疫?

Java 不可变对象是否对不正确的发布免疫?,java,multithreading,immutability,publish,Java,Multithreading,Immutability,Publish,这是我们的一个例子 第34页: [15] 这里的问题不是Holder类本身,而是 持有者未正确发布。然而,持有者可以免疫 通过将n字段声明为final来阻止不正确的发布 会使持有者不可变 和来自: 最终规范(见@andersoj的答案)保证 当构造函数返回时,最后一个字段将被正确地删除 已初始化(从所有线程可见) 发件人: 例如,在Java中,如果对构造函数的调用 如果已内联,则共享变量可能会立即更新一次 存储已分配,但在内联构造函数之前 初始化对象 我的问题是: 因为:(可能是错的,我不知道。

这是我们的一个例子

第34页:

[15] 这里的问题不是Holder类本身,而是 持有者未正确发布。然而,持有者可以免疫 通过将n字段声明为final来阻止不正确的发布 会使持有者不可变

和来自:

最终规范(见@andersoj的答案)保证 当构造函数返回时,最后一个字段将被正确地删除 已初始化(从所有线程可见)

发件人:

例如,在Java中,如果对构造函数的调用 如果已内联,则共享变量可能会立即更新一次 存储已分配,但在内联构造函数之前 初始化对象

我的问题是:

因为:(可能是错的,我不知道。)

a) 在内联构造函数初始化对象之前,可以立即更新共享变量

b) 只有当构造函数返回时,才能保证正确初始化最后一个字段(从所有线程可见)

另一个线程是否可能看到默认值
holder.n
?(即,在
holder
构造函数返回之前,另一个线程获取对
holder
的引用。)

如果是,那么你如何解释下面的陈述

持有者可以通过声明n来避免不适当的发布 字段为最终字段,这将使持有者不可变

编辑: 来自JCiP。不可变对象的定义:

如果:
x之后无法修改其状态 建设

x其所有字段均为最终字段;[12] 及

它是正确的 已构造(此引用在构造期间不会消失)

因此,根据定义,不可变对象不存在“
this
reference-escaping”问题。对吧?


但是如果不声明为volatile,它们会在双重检查锁定模式中受到影响吗?

否,如果构造函数在返回之前泄漏了对该的引用,则仍然可以安全地发布不可变对象(这是启动之前发生的情况)


引用泄漏的两种可能途径是,如果构造函数尝试注册新对象以进行回调(例如,某个构造函数参数上的事件侦听器),或者使用注册表,或者更微妙地,调用被重写的非final方法以执行相同的操作

一个不可变的对象,例如
字符串
,对于所有读卡器来说似乎都具有相同的状态,而不管它的引用是如何获得的,即使同步不正确且缺少关系也是如此

这是通过Java5中引入的
final
字段语义实现的。如中所定义,通过最终字段进行的数据访问具有更强的内存语义

在编译器重新排序和内存障碍方面,在处理最终字段时有更多约束,请参阅。你担心的重新排序不会发生

是的——可以通过包装中的最后一个字段进行双重检查锁定;不需要
volatile
!但这种方法不一定更快,因为需要两次读取


请注意,此语义适用于单个最终字段,而不是整个对象。例如,
String
包含一个可变字段
hash
;然而,
String
被认为是不可变的,因为它的公共行为仅基于
final
字段

最后一个字段可以指向可变对象。例如,
String.value
是一个可变的
char[]
。要求不可变对象是最终字段树是不切实际的

final char[] value;

public String(args) {
    this.value = createFrom(args);
}
只要我们在构造函数退出后不修改
value
的内容,就可以了

我们可以在构造函数中以任何顺序修改
value
的内容,这无关紧要

public String(args) {
    this.value = new char[1];
    this.value[0] = 'x';  // modify after the field is assigned.
}
另一个例子

final Map map;
List list;

public Foo()
{
    map = new HashMap();
    list = listOf("etc", "etc", "etc");
    map.put("etc", list)
}
通过最后一个字段的任何访问都将显示为不可变的,例如,
foo.map.get(“etc”).get(2)

非通过最终字段访问不--
foo.list.get(2)
通过不正确的发布是不安全的,即使它读取相同的目标


这就是设计动机。现在,让我们看看JLS是如何在

冻结
操作在构造函数出口处定义,与分配最终字段时相同。这允许我们在构造函数中的任意位置写入以填充内部状态

不安全发布的常见问题是缺少before(
hb
)关系。即使读看到写,它也不会建立任何其他操作。但是,如果一个volatile read看到一个volatile write,JMM将在许多操作中建立
hb
和一个顺序

final
字段语义想要做同样的事情,即使是正常的读写,也就是说,即使是通过不安全的发布。为此,在读操作看到的任何写操作之间添加内存链(
mc
)顺序

deferences()
顺序将语义限制为通过最后一个字段访问

让我们重温一下
Foo
示例,看看它是如何工作的

tmp = new Foo()

    [w] write to list at index 2

    [f] freeze at constructor exit

shared = tmp;   [a]  a normal write

// Another Thread

foo = shared;   [r0] a normal read

if(foo!=null) // [r0] sees [a], therefore mc(a, r0)

    map = foo.map;          [r1] reads a final field

    map.get("etc").get(2)   [r2]
我们有

hb(w, f), hb(f, a), mc(a, r1), and dereferences(r1, r2)
因此,
w
r2
可见


本质上,通过
Foo
wrapper,一个映射(其本身是可变的)可以通过不安全的发布安全地发布。。。如果这有道理的话

我们可以使用包装器建立最终字段语义,然后丢弃它吗?像

Foo foo = new Foo();   // [w] [f]

shared_map = foo.map;  // [a]
有趣的是,JLS包含了足够多的条款来排除这种用例。我猜它被削弱了,因此允许进行更多的内线程优化,即使使用final字段也是如此

final char[] value;

public String(args) {
    this.value = createFrom(args);
}

请注意,如果在冻结操作之前泄漏了此,则最终字段语义为n
-- class Bar

final int x;

Bar(int x, int ignore)
{
    this.x = x;  // assign to final
}  // [f] freeze action on this.x

public Bar(int x)
{ 
    this(x, 0);
    // [f] is reached!
    leak(this); 
}