【问题标题】:Is it thread-safe to access outer field (non-final) from inner class thread?从内部类线程访问外部字段(非最终)是否是线程安全的?
【发布时间】:2013-01-16 09:06:20
【问题描述】:

基本上以下方法有效,但由于我阅读了 final 关键字,我不确定如果不同线程访问它是否必须声明 name final?

提前致谢。

public class Test4 {

    // to ensure thread-safety do we have to declare the variable name final ?
    private String name;

    public Test4 (String name) {
        this.name = name;
    }

    public void start() {
        new MyThread().start();
    }

    private class MyThread extends Thread {

        public void run() {
            System.out.println(name);
        }
    }

    public static void main(String[] args) {
        Test4 t = new Test4("Don't know if I am threadsafe");
        t.start();
    }

}

【问题讨论】:

  • 这将始终打印“不知道我是否是线程安全的”,无论最终结果如何。直到对象被构造出来,new() 才会返回;这将在新线程启动之前发生。
  • @pst 在进一步写入name 的情况下,是的,存在差异。然而,如果name 的唯一写入时间是在构造期间,则永远不需要final(尽管它确实使读者的意图更清楚)。

标签: java multithreading thread-safety


【解决方案1】:

final 修饰符 - 在防止重新分配成员的同时 - 不 影响 给定 代码的正确性1

来自 Java 5 语言规范的 17.4.4 Synchronization Order 部分:

同步顺序是执行的所有同步操作的总顺序。同步操作会导致操作上的同步关系,定义如下:

  • ..
  • 启动线程的动作与它启动的线程中的第一个动作同步。
  • ..

那么,由于设置 name 成员的线程是启动线程的线程,因此同步顺序得到保证。 (Synchronizes-with 意味着 Happens-before ordering。)

请注意:

  • 成员name只需要在启动线程之前设置:也就是说,不需要在构造函数中设置它来保证同步。
  • 这不保证同步顺序 - 因此它不保证在已经运行的线程或在其他地方创建的线程之间发生之前或值可见性!

但是,final 字段确实给人一种更舒适的感觉(参考17.5 Final Field Semantics):

当一个对象的构造函数完成时,它被认为是完全初始化的。一个线程只能看到对该对象的引用*在该对象已完全初始化之后保证看到该对象的最终字段的正确初始化值。

在这种情况下,使用 final 字段,可以保证在构造函数完成后,该值在 每个 线程上可见。 ("constructor leaks" 可能违反此保证。)


1在提供的代码中,“非最终”name 成员仅在线程启动之前分配一次。

在不同中,不那么琐碎的程序可能会暴露其他同步问题。此答案检查删除 final 是否会改变所提供代码的正确性。

话虽如此,我认为同时使用不可变变量 (final) 和不可变对象是“好习惯”——尤其是在处理线程时。无需了解 JVM 的一些晦涩难懂的细节,而是做一些经过验证的安全事情,并争取明显的正确性,而不是聪明或“性能”。

另见:

【讨论】:

  • 过于复杂的答案(注意对象的 start() 方法仅在构建后调用,因此会看到完全构建的状态,就足够了),但正确且完整。跨度>
【解决方案2】:

final 变量为immutable,一旦构造完成就无法更改值,因此不存在并发问题。

【讨论】:

  • 虽然是真的,但最终变量评估为可变对象可能最终会出现一个奇怪行为的世界......所以在这种情况下它是真的,因为 变量是最终的 并且 字符串对象是不可变的。 (但是,要回答它是否是线程安全的问题,需要知道非静态 final member 字段的发生之前,而且我并不完全确定 final - 或缺乏 -实际上改变了这种行为。)
  • 所以final 确实在构造函数和线程方面做了一些事情(参见Final Field Semantics);不过,这是通过保证 JVM 语义实现的,不是只是“因为它不能被重新分配”。
  • 感谢语义,我同意最终变量完整初始化的“使用模型”。
【解决方案3】:

没有final你不会得到正确的字段值。

在你更改字段的值后,线程可能得到了旧值。

查看JMM的Visibility。

Another link of volatile.

Happends-Before Rule.

Happends-Before in JMM.

【讨论】:

  • 我不相信这是正确的。也就是说,在这种情况下,线程不会 starting 导致“发生在之前”吗? (虽然我目前找不到支持信息。)
  • 啊,我想这里:stackoverflow.com/questions/7651226/…(但我可能读错了)
【解决方案4】:

您是否正在寻找AtomicReference 或volatile?这取决于您所说的线程安全是什么意思?

// Atomic to allow deeper control of updates.
private AtomicReference<String> name = new AtomicReference<String>();
// Volatile to ensure it is not cached.
private volatile String vName;

public Test(String name) {
  this.name.set(name);
  this.vName = name;
}

public void start() {
  new MyThread().start();
}

private class MyThread extends Thread {
  public void run() {
    System.out.println(name.get());
    System.out.println(vName);
  }
}

【讨论】:

    【解决方案5】:

    final 与多线程无关,但如果您的字段不应更改并在类的构造函数中初始化,则应输入 final。这意味着 fild 不能在后面更改。

    【讨论】:

    • final 确实添加了多线程保证,看来;在这种情况下,最终字段值保证在构造函数存在后可见(由其他线程)。
    • 这个保证是不明显的,因为假设其他线程在构造函数开始执行时可能已经或可能没有完成读取final字段的部分。所以这种特殊的竞争条件仍然存在。
    【解决方案6】:

    由于 String 是不可变的,并且您声明了 final 字段,所有线程在字段分配后访问它,那么肯定不会有并发问题,因为该字段仅用于读取操作,与 StringBuilder 的情况相反被使用了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多