微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

不可变物体是否可以不受不当出版的影响?

如何解决不可变物体是否可以不受不当出版的影响?

一个不可变的对象,例如String,对于所有读者来说似乎都具有相同的状态,而不管它的引用如何获得,即使同步不正确并且没有先发生后关系。

这是通过finalJava5中引入的字段语义实现的。通过最后一个字段进行的数据访问具有更强的内存语义,如jls-17.5.1中所定义。

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

是的-可以通过包装器中的final字段进行双重检查锁定;没有volatile要求!但是这种方法不一定更快,因为需要两次读取。

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

final字段可以指向可变对象。例如,String.valuea 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)
}

final字段 的任何访问都将是不可变的,例如foo.map.get("etc").get(2)

通过最终字段进行访问 不会 - foo.list.get(2)通过不正确的发布是不安全的,即使它读取的是同一目的地。

这些就是设计动机。现在让我们看看JLS如何在jls-17.5.1中对其进行形式化

freeze在构造函数的出口处定义了一个动作,与在final字段的分配中定义的动作相同。这使我们可以在构造函数内部的任何地方编写以填充内部状态。

不安全发布的常见问题是缺少“事前”(hb)关系。即使读取看到写入,它也不会与其他操作建立任何关系。但是,如果易失性读取看到易失性写入,则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包装器,可以通过不安全的发布安全地发布地图(本身是可变的)……如果这是有道理的。

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

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

shared_map = foo.map;  // [a]

有趣的是,JLS包含足以排除此类用例的子句。我猜想它已经被削弱了,所以允许更多的内部线程优化,即使对于最终字段也是如此。

注意,如果this在冻结操作之前泄漏了,则不能保证最终字段的语义。

但是,通过冻结构造函数,我们 可以 在冻结动作 之后 安全地泄漏this到构造函数中。 __

-- 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); 
}

就此x而言,这是安全的;冻结动作x是在x分配了构造函数的地方定义的。这可能只是为了安全泄漏而设计的this

解决方法

这是来自JCiP的示例。

public class Unsafe {
    // Unsafe publication 
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }
    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("This statement is false.");
        }
    }
}

在第34页上:

[15]这里的问题不是Holder类本身,而是Holder没有正确发布。但是,可以通过将n字段声明为final来使Holder免受不适当发布的影响,这将使Holder不变。

从这个答案:

final的规范(请参阅@andersoj的答案)保证当构造函数返回时,将对final字段进行适当的初始化(从所有线程可见)。

维基

例如,在Java中,如果已经内联了对构造函数的调用,则一旦分配了存储空间,但是在内联的构造函数初始化对象之前,可以立即更新共享变量。

我的问题是:

因为:(可能不正确,我不知道。)

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

b)只有在构造函数返回时,才能保证对final字段进行正确的初始化(从所有线程可见)。

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

如果是这样,那么您如何解释以下声明?

通过将n字段声明为final,可以使Holder免受不适当发布的影响,这将使Holder不可变

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

在以下情况下对象是不可变的:
x构造后无法修改其状态;

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

x构造正确(该参考在构造期间不会逸出)。

因此,根据定义,不可变对象没有“ this引用转义”问题。对?

但是如果不声明为易失性,它们是否会遭受双重检查锁定模式的乱序写入

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。