我知道您可以通过编写对ld
或final
字段的引用来安全地发布非线程安全对象,该字段稍后将被其他一个线程读取,前提是在发布时,创建对象的线程会丢弃对它的引用,这样它就不会再干扰或不安全地观察对象在另一个线程中的使用。
但在此示例中,没有明确的volatile
字段,只有final
个局部变量。 如果来电者放弃对final
的引用,这是安全的出版物吗?
unsafe
我发现了一些Q& As,就像this one一样,建议将void publish(final Unsafe unsafe) {
mExecutor.execute(new Runnable() {
public void run() {
// do something with unsafe
}
}
}
个局部变量隐式“复制”到匿名类中。这是否意味着上面的例子与此相当?
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
。
答案 0 :(得分:4)
虽然,诚然,我并不完全确定我得到了你问题的实际观点,并且(正如评论中所指出的)这个问题在你的特定情况下可能不是真正的问题,也许相关的见解可以从测试/示例中获得
考虑以下课程:
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,并在构造函数中初始化。
(这并不完全等同于您发布的第二个代码段,因为第二个代码片段包含两个最终字段,并且它们都使用相同的值初始化。但是关于安全问题出版,效果应该相同)。
答案 1 :(得分:3)
您在第一个示例中留下了一些重要代码:mExecutor
对象可能拥有BlockingQueue
。 mExecutor.execute(r)
调用可能会调用q.put(r)
将您的任务添加到队列中,然后稍后,工作线程调用r=q.take()
以获取任务,然后才能调用r.run()
。
阻塞队列的put()
和take()
方法将在两个线程中的事件之间建立相同类型的“发生之前”关系,这两个线程将由“安全发布”成语之一建立
在q.put(r)
调用返回之前,保证在调用q.take()
之前内存中的第一个线程更新是可见的。
答案 2 :(得分:1)
这个答案似乎可以部分回答这个问题:
Java multi-threading & Safe Publication
至少关于&#34;安全发布&#34;。
现在,如果调用者丢弃其引用,则该变量将是安全的,因为除了最终的局部变量之外,不存在对该变量的引用。
关于代码示例 - 在我看来,两个代码片段都是等效的。引入另一个局部变量并不会改变语义,在这两种情况下,编译器都会将引用识别为不可变,并允许您使用它。
编辑 - 我要离开这部分来记录我对OP问题的误解
为了澄清 - 我在本例中使用了final
或volatile
,因此适当的内存屏障可以满足对象引用的可见性,唯一的一点是可能的可变性使用内存屏障无法保证的非线程安全对象,实际上与它们无关。可以通过适当的同步或只留下一个对内容的引用来处理它。
EDIT2 - 阅读OP的评论后
我刚刚查看了JSR 133 FAQ - AFAIU,使用内存屏障对对象的引用的安全发布并不能保证所提到的引用对象的非同步字段也是可见的。无论是final
还是volatile
。
如果我没有误解这个常见问题解答,那么只有在同一个监视器上进行同步才能为所有定义一个线程在释放同步锁之前执行的写操作的“先发生”关系并获取锁定另一个线程监控。
我可能会弄错,但听起来好像引用对象的非同步字段也是可见的。
如果使用final
关键字(就像在您的示例中将参数作为final
字段插入) - 只保证引用对象的实例字段final
为BlockingQueue
在物体构造结束后可见。
但在LinkedBlockingQueue
(以及synchronized
的实现)中,我看不到任何volatile
个关键字 - 它似乎使用一些非常聪明的代码来实现同步,方法是使用{{ 1}}字段,对我而言,它听起来不像JSR 133中描述的监视器上的同步。
这意味着Executor使用的常见阻塞队列无法保证Unsafe
个实例的非最终字段的可见性。虽然可以仅使用final
关键字安全地发布引用本身,但是此引用所指向的字段的安全发布也要求字段为final
,或者与共享的监视器同步作家和读者。
不要射击使者: - )。