7

我正在阅读关于线程和锁的 JLS 文档http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5

class FinalFieldExample { 
final int x;
int y; 
static FinalFieldExample f;

public FinalFieldExample() {
    x = 3; 
    y = 4; 
} 

static void writer() {
    f = new FinalFieldExample();
} 

static void reader() {
    if (f != null) {
        int i = f.x;  // guaranteed to see 3  
        int j = f.y;  // could see 0
    } 
} 
}

我对本节中提到的上述示例(例如第 17.5-1 号)感到困惑,关于如何将 fy 视为零。Reader Threads 要么将对象 f 读取为 null,在这种情况下它不会执行任何操作,要么它将读取带有一些引用的对象 f。如果对象 f 有一个引用,那么即使多个写入线程正在运行,构造函数也必须完成其执行,以便可以将引用分配给 f,如果构造函数已经执行,则 fy 应该被视为 4。

在什么条件下 fy =0 是可能的?

谢谢

4

4 回答 4

5

在什么条件下 fy =0 是可能的?

Java 内存模型允许 JIT 编译器在构造函数之外重新排序非 final字段的初始化。该字段是最终的,因此它必须由 JVM 初始化,但不是最终的。所以有可能是分配并设置了,但是字段的初始化还没有完成。xyFinalFieldExamplestatic FinalFieldExample fy

引用 17.5-1:

因为 writer 方法在对象的构造函数完成后写入 f,所以 reader 方法将保证看到正确初始化的 fx 值:它将读取值 3。因此,不能保证 reader 方法看到它的值 4。

因为f.y不是最终的,所以不能保证它在构造函数完成并static f分配时已经设置。因此,创建了一个竞争条件,根据这场比赛,reader可能会看到3 或 0。y

于 2013-02-07T20:44:37.490 回答
2

如果一个线程写入一个变量而另一个线程读取它,那么即使读取发生在稍后的时间,第二个线程也可能看不到新值。例如,如果两个线程在不同的处理器上执行,并且写入的值缓存在本地处理器寄存器中,则可能会发生这种情况。

Java 规范使这种非直观的行为成为可能,以提高性能(如果这不可能,那么处理器就不能使用它们的本地内存)

因此,每当您阅读 Java 内存模型中的“之前发生”关系时,请记住,“物理”之前发生在程序逻辑中不一定是“发生之前”。您需要明确地建立两个线程之间的“之前发生”关系,例如通过同步、使用 volatile 变量或在本例中使用 final 变量。

于 2013-02-07T20:36:20.607 回答
1

这不仅仅是指令重新排序。即使对 fy 的写入在对 f 的写入之前,它也可能发生。对象和类都是人类的烟雾和镜子。在 CPU 级别,它都是加载内存位置和存储内存位置。数据最初进入 CPU 缓存。让我们假设在这种情况下 f 进入一个高速缓存行,而 fy 进入另一个高速缓存行。writer 线程执行其他操作以使第一个高速缓存行(保持 f)对其余 CPU 可见。拿着 fy 的那个还不可见(没有指示 CPU 这样做)。内存位置仍然为 0。当读取器线程在不同的 CPU 上运行时,它将加载内存位置,因为没有任何东西告诉 CPU 该位置在另一个 CPU 缓存中有待处理的更改。这意味着第二个 CPU 将从内存中加载 f 和 fy。f 保存最新的值,但最新的 fy 仍在缓存中,因此内存位置为 0。通过放置 volatile、final 等,您实际上是在告诉编译器生成告诉 CPU 发布数据的代码。这只是一个例子,还有更多。

于 2013-02-07T20:43:14.037 回答
0

f.y可以看成0如下方式:

假设两个threadsT1 和 T2 正在运行。T1 正在访问该方法writer,而 T2正在访问reader同一对象的方法FinalFieldExample

  1. T1 调用方法writer()f.x已经初始化为 3,因为它被声明为 final,这使它成为编译时间常数,因此它在类的初始化期间被初始化为给定值,FinalFieldExample这发生在类实例的创建之前。由于f没有这样声明,volatile因此编译器以下列方式重新调整创建对象的步骤顺序:(a)f声明为非空(b)FinalFieldExample 创建的对象(b)f是对该对象的引用。但是在点 (a) 被 T1 执行后, T1 被 T2 抢占
  2. T2 调用方法reader()。它发现f不为空,因此它进入 if 块。f.x已初始化为,3因此i已分配 value 3。但是f.y直到现在初始化为默认值0,因为在上面给出的步骤 1 中没有完成对象的构造。j赋值为 0 也是如此。

所以我们看到,不将变量声明f为 volatile 可以 compiler自由地优化代码,以便在执行时内联调用constructor并且共享变量f在线程之间共享,T1并且T2一旦分配了存储空间就可以立即更新,但是在内联构造函数初始化对象之前。

于 2013-02-07T20:48:21.117 回答