【问题标题】:Safe publication of local final references本地最终参考文献的安全发布
【发布时间】:2016-11-10 03:45:16
【问题描述】:

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

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

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

我发现了一些问答,例如this one,表明final 局部变量被隐式“复制”到匿名类中。是不是说上面的例子就和这个等价了?

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合约的东西。

【问题讨论】:

  • 您的执行程序正在使用线程安全队列。要查看此问题,您必须在线程之间传递对象,而无需在任何地方使用适当的内存屏障。
  • @PeterLawrey class Executor { void execute(Runnable r) { } } - 这里没有队列。但这一点可能仍然有效......
  • BTW Unsafe 是一个有单例的类,虽然你可以创建更多...
  • @PeterLawrey 我不是指sun.misc.Unsafe。为清晰起见进行了编辑。

标签: java multithreading safe-publication


【解决方案1】:

虽然,诚然,我不完全确定我得到了您问题的实际意义,并且(正如 cmets 中所指出的)在您的特定情况下,这个问题可能并不是真正的问题,也许可以从测试/示例中获得相关见解

考虑以下类:

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$1.class 内部类

反编译内部类的类文件会产生以下结果:

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

  final SafePublication this$0;

  SafePublication$1(SafePublication, Unsafe);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0: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参数,确实有这个类中引入了一个字段。该字段为val$unsafe,为final field in the class file sense,在构造函数中初始化。

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

【讨论】:

  • 这正是我想要的。
【解决方案2】:

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

阻塞队列的put()take() 方法将在两个线程中的事件之间建立相同类型的“发生之前”关系,这将由“安全发布”习惯用法之一建立。

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

【讨论】:

  • 我认为您提出的论点在这里忽略了要点:OP 明确谈论非线程安全对象,“发生在”语义有助于设置引用,但不反对通过调用者 Runnable 已被放入队列并且可能与在 Runnable 中使用它的同时。因此,事实上,在访问相关的局部变量方面,它在线程安全和安全发布方面完全没有任何保证。
  • @TomaszStanczak,OP 似乎说这不会发生,“......假设在发布时,创建对象的线程丢弃对它的引用,因此它不能再干扰或不安全地观察对象在另一个线程中的使用。”
  • 这就是保证,而不是putget 的“发生在之前”的关系,它本身不能提供这种保证。其实函数调用已经有了这种关系,发生的是:1.调用publish,2.put,3.get。如果原始变量引用被丢弃,无论接下来发生什么,调用 publish 就足够了。
  • @jameslarge 关键是以某种创建内存屏障的方式传递引用,因此可以保证先前写入发布线程中未同步字段的值对接收线程可见。放弃引用本身与此无关;它只是避免在发布线程中随后的不安全使用。我认为BlockingQueue 可以提供这种保证是正确的,任何实用的Executor 可能确实有BlockingQueue 或类似的东西,但我不确定。
  • @jameslarge 显然是我错过了重点:-) - 丢弃引用没有受到质疑,内存障碍被问及,而不是相反......
【解决方案3】:

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

Java multi-threading & Safe Publication

至少关于“安全发布”。

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

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

编辑 - 我离开这部分是为了记录我对 OP 问题的误解

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

EDIT2 – 阅读 OP 的 cmets 后

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

如果我没有误解此常见问题解答,则仅在同一监视器上同步为 所有 一个线程在释放同步锁并在同一监视器上获取锁之前所做的写入定义了“发生前”关系由另一个线程监控。

我可能弄错了,但听起来好像被引用对象的非同步字段也是可见的。

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

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

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

不要向信使开枪:-)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-22
    • 2010-09-26
    • 1970-01-01
    相关资源
    最近更新 更多