【问题标题】:Thread safety of final fieldfinal 字段的线程安全
【发布时间】:2013-05-23 08:34:30
【问题描述】:

假设我有一个从另一个线程更新的 JavaBean User,如下所示:

public class A {

    private final User user;

    public A(User user) {
        this.user = user;
    }

    public void aMethod() {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                ...a long running task..
                user.setSomething(something);
            }

        });
        t.start();
        t.join();
    }

    public void anotherMethod() {
        GUIHandler.showOnGuiSomehow(user);
    }
}

这个代码线程安全吗?我的意思是,当创建 A 实例并调用 A.aMethod 的线程读取用户字段时,它是否看到用户处于新鲜状态?如何以适当的线程安全方式执行此操作?

请注意,我无法修改用户类,也不知道它本身是否是线程安全的。

【问题讨论】:

    标签: java multithreading thread-safety


    【解决方案1】:

    这个代码线程安全吗? ...它看到用户处于新鲜状态吗?

    不是特别是 - user 在您的代码中是 final 的事实除了不能被替换之外对线程安全几乎没有影响。

    应该更改的位是由setSomething 设置的实例变量。它应该被标记为volatile

    class User {
      // Marked `volatile` to ensure all writes are visible to other threads.
      volatile String something;
    
      public void setSomething(String something) {
        this.something = something;
      }
    
    }
    

    但是,如果(如您所建议的)您无权访问 User 类,则必须执行创建内存屏障的同步。在最简单的形式中,您可以使用synchronized 访问权限来包围您对user 的访问权限。

    synchronized (user) {
      user.setSomething(something);
    }
    

    添加:- 事实证明(参见here)实际上可以这样完成:

    volatile int barrier = 0;
    ...
    user.setSomething(something);
    // Forces **all** cached variable to be flushed.
    barrier += 1;
    

    【讨论】:

    • 假设我不能以任何方式修改 User 类。
    • @Behnil - 答案扩展。
    【解决方案2】:

    将字段标记为final 只是意味着引用 不能更改。这对User 类的线程安全性没有任何意义。如果此类访问字段的方法是同步的(或使用其他同步技术),则它是线程安全的。否则不是。

    【讨论】:

      【解决方案3】:

      final 只会使引用不可重新分配,但如果引用指向可变类,您仍然可以更改该对象内部的状态,这会导致不安全。

      只有当 User 类是不可变的时,你的代码才是线程安全的,即User 的所有属性都不能在对象之外更改,类中的所有引用都指向其他不可变类。

      如果不是这样,那么你必须正确同步它的方法以使其线程安全。

      【讨论】:

        【解决方案4】:

        请注意,我无法修改用户类,也不知道它本身是否是线程安全的。

        您必须在访问 User 对象时同步您的访问权限。 例如,您可以使用 User 对象进行同步,因此只需将用户对象上的每个访问都包装为以下内容:

        synchronized(user) {
          // access some method of the user object
        }
        

        假设用户对象只能在你的线程中异步访问。还要保持同步块简短。

        您还可以围绕用户对象构建一个线程安全的包装器。我建议如果你有很多不同的调用,那么代码会变得更清晰,更好地阅读。

        祝你好运!

        【讨论】:

          【解决方案5】:

          关于线程,finalfields 只是保证在constructor escape 的情况下是一致的,因为JSR-133 关于内存屏障机制:

          对象的最终字段的值在其构造函数中设置。 假设对象是“正确”构造的,一旦对象被 构造,分配给最终字段的值 构造函数将对所有其他线程可见,而无需 同步。此外,任何其他对象的可见值 或那些最终字段引用的数组将至少为 最新的作为最终字段。一个对象是什么意思 正确构建?它只是意味着没有对对象的引用 在施工过程中允许正在施工的“逃生”。 (看 安全施工技术示例。)换句话说,不要 在任何地方放置对正在构造的对象的引用 另一个线程可能能够看到它;不要将其分配给静态 字段,不要将其注册为任何其他对象的侦听器,因此 在。这些任务应该在构造函数完成后完成,而不是在 构造函数。

          但是,没有任何东西可以确保在剩余对象的生命周期中(即在包装类的构造函数执行之后)任何最终字段的自动线程安全。。确实,Java 中的不可变性完全是用词不当:

          现在,用普通话来说,不变性意味着“不变”。 在 Java 中,不变性并不意味着“不变”。意思是“是 从最终字段传递可达,自 设置了 final 字段,并引用了包含 final 字段没有逃脱构造函数”。

          【讨论】:

          • 我不认为给定的不变性定义是完全正确的。如果对对象的引用存储在final 字段中,但也存在对该对象的其他引用,我认为从任何引用的角度来看,该对象都不会是“不可变的”未从final 字段读取或复制.
          【解决方案6】:

          是的,这是安全的。见

          Java Language Specification (Java 8) Chapter 17.4.4:

          线程 T1 中的最终操作与另一个线程 T2 中检测到 T1 已终止的任何操作同步。

          T2 可以通过调用 T1.isAlive() 或 T1.join() 来完成此操作。

          把这个和17.4.5. Happens-before Order放在一起:

          两个动作可以通过happens-before关系排序。如果一个动作发生在另一个动作之前,那么第一个动作对第二个动作可见并在第二个动作之前排序。 [..] 如果动作 x 与后续动作 y 同步,那么我们也有 hb(x, y)。

          因此,在您在代码中调用 t.join(); 后,您将看到更新后的更改。由于“创建A实例并调用A.aMethod的线程”在调用aMethod之后和调用t.join之前不可能读取值(因为它正忙于方法aMethod),所以这是安全的。

          【讨论】:

            猜你喜欢
            • 2023-03-19
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-04-02
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多