Java 未删除易失性字段和局部变量

Java 未删除易失性字段和局部变量,java,synchronization,volatile,java-memory-model,Java,Synchronization,Volatile,Java Memory Model,考虑下面的代码片段 class Person { private final String firstName; private final String lastName; Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } String getFirstName() {

考虑下面的代码片段


class Person {
    private final String firstName;
    private final String lastName;

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    String getFirstName() {
        return this.firstName;
    }

    String getLastName() {
        return this.lastName;
    }

}

class PersonPrinter {

    private Person person;

    void print() {
        Person p = this.person;
        System.err.println(p.getFirstName());
        System.err.println(p.getLastName());
    }

    void setPeron(Person person) {
        this.person = person;
    }

}
假设在同一
PersonPrinter
实例上有多个线程同时调用方法
setPerson()
print

因为代码没有正确同步,所以只要保持“仿佛串行”语义,JVM就可以引入优化

尤其是
print()
方法可以重新安排为:

    void print() {
        System.err.println(this.person.getFirstName());
        System.err.println(this.person.getLastName());
    }
这是为了直接访问字段而删除局部变量:这不会改变单线程语义。当然,这种优化可能会导致打印一个人的名字和另一个人的姓氏(因为另一个并发线程可以调用
setPerson()

我觉得在PersonPrinter中将
person
字段标记为volatile可以解决这个问题,并防止本地文件被删除(从而保证我们打印同一个人的名字和姓氏)。。。。但我找不到oracle文档中出现这种情况的原因:通过查看java内存模型的规则,是什么阻止了删除局部变量而直接访问易失性字段

尤其是
print()
方法可以重新安排为:

    void print() {
        System.err.println(this.person.getFirstName());
        System.err.println(this.person.getLastName());
    }

作废打印(){
System.err.println(this.person.getFirstName());
System.err.println(this.person.getLastName());
}
。。。当然,这种优化可能会导致打印一个人的名字和另一个人的姓氏(因为另一个并发线程可以调用
setPerson()

这种新的行为(因此这种优化)是非法的,因为它违反了Java内存模型(JMM)

发件人:

内存模型描述程序的可能行为。一个实现可以自由地生成它喜欢的任何代码,只要程序的所有结果执行产生的结果可以由内存模型预测

在原始代码中,
此.person在
print()
中访问一次,因此它对应于单个
读取
操作

您的优化版本的
print()
会读取
this.person
两次,并获得不同的值。

这种行为不能用单个的
读取
操作来解释,因此它违反了JMM。

对不正确同步的程序进行推理是非常困难的

正如您已经指出的,代码没有正确同步。确切地说,打印机上存在数据竞争。人员字段:至少有2个线程可以访问此字段,其中至少有一个线程是写线程

当存在数据竞争时,会出现未定义的行为。从这一点上讲,对这种行为进行推理是非常徒劳的

如果将Printer.person字段设置为volatile,则数据争用将消失,因为在字段的写入和读取之间引入关系之前发生了冲突(volatile variable rule)


你这样做的方式,读取一个局部变量中的对象,然后使用这个对象,是我经常使用的一种技术。因此,即使世界在进步,你也能看到世界处于一个稳定的时间点。

你能为你的推理提供参考吗?我看不到任何关于为什么可以省略局部变量的解释,因为这是一个创建“私有”副本的显式操作。据我所知,移除私有副本是允许的(至少在理论上是允许的),因为它不会改变方法的单线程语义:保持了“if serial”语义,如果在一个线程上运行代码,您将无法区分两者之间的区别。也允许使用相反的方式(即本地介绍),但它确实会以您演示的方式更改这些语义。通过制作私有副本,其他任何人都不可能干扰私有副本。除此之外,删除一个局部变量以进行冗余读取将是一个相当不寻常的“优化”,如果你声称允许JVM这样做,你就是证明这一点的人,例如,通过提供一个源。您正在重复这个“仿佛序列”规则,但没有这样的规则表明只要保留序列语义,优化器就可以做任何事情。这个“仿佛串行”很好地解释了为什么不使用线程安全结构会导致奇怪的行为,但它不能作为描述优化器可以做什么的正式规则。局部变量本身是非共享的,因此,如果看到它的值突然发生变化,而该变化必须是由不同的线程完成的,换句话说,是共享行为,则会违反规范。当存在数据竞争时,会出现未定义的行为。从这一点上讲,对这种行为进行推理是非常徒劳的。严格地说,在java中,这种行为是(与字面上未定义的行为相反)。尽管如此,对具有数据竞争的java程序进行推理确实非常困难。