本地最终参考的安全发布

Safe publication of local final references

我知道您可以通过写入对 finalvolatile 字段的引用来安全地发布非线程安全对象,该字段稍后将由另一个线程读取,前提是发布时,创建该对象的线程会丢弃对该对象的引用,因此它不能再干扰或不安全地观察该对象在其他线程中的使用。

但是在这个例子中,没有显式的 final 字段,只有 final 局部变量。 如果调用者放弃其对 unsafe 的引用,此发布是否安全?

void publish(final Unsafe unsafe) {
    mExecutor.execute(new Runnable() {
        public void run() {
            // do something with unsafe
        }
    }
}

我发现了一些问答,比如 this one,建议 final 局部变量隐式 "copied" 到匿名 类。那是否意味着上面的例子等同于此?

void publish(final Unsafe unsafe) {
    mExecutor.execute(new Runnable() {
        final Unsafe mUnsafe = unsafe;
        public void run() {
            // do something with mUnsafe
        }
    }
}

编辑澄清:

Unsafe 可以是任何东西,但可以这样说:

public class Unsafe {
    public int x;
}

mExecutor是满足Executor的任何东西。

这个答案似乎部分回答了这个问题:

Java multi-threading & Safe Publication

至少 "safe publication".

现在,如果调用者放弃其引用,该变量将是安全的,因为除了最终局部变量外,不存在对该变量的引用。

关于代码示例 - 在我看来,这两个代码片段是等效的。引入一个额外的局部变量不会改变语义,在这两种情况下,编译器都会将引用识别为不可变的,并让您使用它。

编辑 - 我将离开这部分来记录我对 OP 问题的误解

澄清一下——我在此示例中使用了 finalvolatile,因此存在满足对象引用可见性的适当内存屏障,唯一的一点是非线程安全对象可能的可变性,使用内存屏障无法保证,实际上与它们无关。可以通过适当的同步或仅保留对内容的一个引用来解决。

EDIT2 – 阅读 OP 的评论后

我刚刚查看了 JSR 133 FAQ - AFAIU 使用内存屏障安全发布对对象的引用并不能保证所提到的引用对象的未同步字段也可见。既不是 final 也不是 volatile.

如果我没有误解此常见问题解答,则仅在同一台监视器上同步为 all 定义了一个“先发生”关系,一个线程在释放同步锁和获取之前执行的操作另一个线程在同一个监视器上的锁。

我可能弄错了,但在我看来,引用对象的非同步字段似乎也是可见的。

如果使用 final 关键字(就像在您的示例中,参数作为 final 字段插入)- 仅保证引用对象的实例字段本身是 final在对象构造结束后可见。

但是在 BlockingQueue(以及它的实现 LinkedBlockingQueue)中,我看不到任何 synchronized 关键字——它似乎使用了一些非常聪明的代码来实现同步,方法是使用volatile 字段,对我来说,这听起来不像是 JSR 133 中描述的监视器上的同步。

这意味着 Executor 使用的公共阻塞队列不保证 Unsafe 实例的非最终字段的可见性。虽然引用本身可以仅使用 final 关键字安全发布,但安全发布此引用指向的字段需要字段也为 final,或者与共享的监视器同步作者和 reader.

不要开枪 :-)。

您在第一个示例中遗漏了一些重要代码:mExecutor 对象可能拥有一个 BlockingQueuemExecutor.execute(r) 调用可能会调用 q.put(r) 将您的任务添加到队列中,然后稍后,工作线程会调用 r=q.take() 来获取任务,然后才能调用 r.run()

阻塞队列的 put()take() 方法将在两个线程中的事件之间建立相同类型的 "happens before" 关系,该关系将由 "safe publication"成语.

无论第一个线程在调用 q.put(r) 之前在内存中更新什么,都保证在 q.take() 调用 returns 之前对第二个线程可见。

虽然,诚然,我不完全确定我是否理解了你问题的真正要点,并且(正如评论中指出的那样)这个问题可能并不是你的特定问题案例,也许可以从test/example

中获得相关见解

考虑以下因素class:

import java.util.concurrent.ExecutorService;

class Unsafe
{

}

class SafePublication
{
    private final ExecutorService mExecutor = null;

    public void publish(final Unsafe unsafe)
    {
        mExecutor.execute(new Runnable()
        {
            @Override
            public void run()
            {
                // do something with unsafe
                System.out.println(unsafe);
            }
        });
    }
}

可以编译,得到两个.class个文件:

  • SafePublication.class
  • SafePublication.class 为内部 class

为内部 class 反编译 class 文件会产生以下内容:

class SafePublication implements java.lang.Runnable {
  final Unsafe val$unsafe;

  final SafePublication this[=11=];

  SafePublication(SafePublication, Unsafe);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this[=11=]:LSafePublication;
       5: aload_0
       6: aload_2
       7: putfield      #2                  // Field val$unsafe:LUnsafe;
      10: aload_0
      11: invokespecial #3                  // Method java/lang/Object."<init>":()V
      14: return

  public void run();
    Code:
       0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #2                  // Field val$unsafe:LUnsafe;
       7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      10: return
}

可以看出,对于final参数,确实在这个class中引入了一个字段。这个字段是val$unsafe,是一个final field in the class file sense,在构造函数中初始化

(这并不完全等同于您发布的第二个代码片段,因为第二个代码片段包含 两个 最终字段,并且它们都使用相同的值初始化。但是关于安全发布的问题,效果应该是一样的)。