【问题标题】:Visibility Guarantee能见度保证
【发布时间】:2015-09-08 18:07:32
【问题描述】:

我已经阅读了JCIP对第16.3节“初始化安全”的一些解释,但仍然不清楚。该部分指出

“此外,任何可以通过正确构造的对象的 final 字段到达的变量(例如 final 数组的元素或 final 字段引用的 HashMap 的内容)也保证是对其他线程可见。”

如果我有以下可变对象:

public final class Container{
    private String name;
    private int cupsWon;
    private double netWorth;

        public Container( String name, int cupsWon, double netWorth ){
             this.name = name;
             this.cupsWon = cupsWon;
             this.netWorth = netWorth;
        }

    //NO Setters
    //Getters
}

然后,Thread 1 如下创建它并将 c 传递给 Thread2

final Container c = new Container("Ted Dibiasi", 10, 1000000);

Thread2(不是并发的,假设在 1 ms 之后),读取 c 的值,Thread2 是否有可能看到

c.name=null or
c.cupswon=0 or worst of all, 
c.netWorth=0.0?

干杯

更新

我注意到有一些关于类有吸气剂的困惑。 我正在更新源代码,希望这会很清楚。 感谢大家观看。

public final class Container{

    private String name;
    private int cupsWon;
    private double netWorth;

    public Container( String name, int cupsWon, double netWorth ){
        this.name = name;
        this.cupsWon = cupsWon;
        this.netWorth = netWorth;
    }

    public final String getName(){
        return name;
    }

    public final int getCupsWon(){
        return cupsWon;
    }

    public final double getNetWorth(){
        return netWorth;
    }

}

//---------

public final class Producer{

    private final Client client;

    public Producer( Client client ){
         this.client = client;
    }

    //Thread1 call produce()   
    public final void produce( ){
        final Container c = new Container("Ted Dibiasi", 10, 1000000);
        client.update( c );
    }

}

//----

public final class Client{

     private Container c;
     //private volatile Container c;       

     public final void update( Container c ){
          this.c = c;
     }

     //Thread2 calls consume().
     public final void consume( ){
          String name = c.getName();
          int cupsWon = c.getCupsWon();
          double netWorth = c.getNetWorth();           
     }

 }

我的问题是:

a) 当Thread2 调用consume() 时,name、cupsWon、netWorth 可以为null、0 还是0.0?我的想法是它可以,因为容器类中的字段不是最终的,所以没有可见性保证。

b) 但是,然后我阅读了第 16.3 节和关于“可以通过正确构造的对象的最终字段到达的变量”的部分,这是否意味着因为容器 c 的实例被宣布为最终的,我们确实在consume()中有可见性保证吗?

final 容器 c = new Container("Ted Dibiasi", 10, 1000000);

c) 将 Client 类中对 Container 的引用声明为 volatile 不会解决与引用相关的字段的可见性问题。

【问题讨论】:

  • 由于构造是在通过之前发生的(否则如何才能通过?),我猜这是不可能的。
  • 你能澄清一下是否有吸气剂吗?我认为您的意思是 getter 但 no setter,而有一个答案假设两者都没有。
  • @hiergiltdiestfu 是的,有 getter,但没有 setter。
  • 此引用不适用于此处,因为没有最终字段。类的 final 不会影响各个字段。
  • @NathanHughes 我的想法很准确。由于没有一个字段是最终的,因此无法保证另一个线程将看到更新的值。我只是希望 JCIP 中的第 16.3 节措辞清楚一点。 ——

标签: java multithreading concurrency memory-visibility


【解决方案1】:
final Container c = new Container("Ted Dibiasi", 10, 1000000);

如果c 这里是Thread1 中的最后一个字段而不是局部变量,那么来自Java Language Specification 的引用适用于这个最后一个字段c

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

final 字段的使用模型很简单:在对象的构造函数中设置对象的 final 字段;并且不要在对象的构造函数完成之前在另一个线程可以看到它的地方写入对正在构造的对象的引用。如果遵循这一点,那么当另一个线程看到该对象时,该线程将始终看到该对象的最终字段的正确构造版本。 它还将查看那些最终字段引用的任何对象或数组的版本,这些版本至少与最终字段一样最新。

虽然这里的措辞很模糊,但我认为“正确初始化值”和“最新作为最终字段”意味着如果你通过cThread1 构造函数之外的 Thread2Thread2 将始终看到一个完整构造的 Container 实例,其字段已初始化。

【讨论】:

  • 但是字段不是最终的!
  • 本例中的最终字段是final Container c = new Container("Ted Dibiasi", 10, 1000000);
  • 所以容器实例是Thread1的final字段引用的对象。如果Thread1构造函数完成,内存模型保证它至少是最新的作为最终字段c
  • 我刚刚注意到final Container c = new Container("Ted Dibiasi", 10, 1000000); 也可能是一个局部变量。在这种情况下,报价不适用。但从问题上下文来看,它似乎是Thread1 中的一个字段。
【解决方案2】:

程序员通常不需要担心这个问题。仅当对象“不安全地发布”时才会出现问题,例如,对象由 Thread-1 分配给非易失性静态字段,而 Thread-2 通过读取非易失性字段来检索对象。但是,这种情况很少见;对象在线程之间传递几乎总是带有一些内存屏障。例如,当您将对象传递给 ThreadPoolExecutor 时,您无需担心可见性。

应不惜一切代价避免不安全的发布,除非您真的需要它并且您确切地知道自己在做什么。

一个类通常不需要设计成能够承受不安全的发布,除非有充分的理由。例如,String 就是这样设计的,因为它在核心安全/访问控制代码中被广泛使用,并且字符串的内容必须看起来是恒定的,即使某些恶意程序试图通过不安全的发布来破坏它。

为了承受不安全的发布,大多数类不需要使用final 字段。

【讨论】:

    【解决方案3】:

    回答你的问题,不,Thread2 永远不会看到 Container 的字段处于未初始化状态。原因是 Container 的构造函数在 引用 Container c 变得可访问之前完全运行。调用Client.updateClient.consume 之间可能存在竞争条件。根据这场竞赛的结果,c 字段要么为空,要么在 Client.consume 中调用 c.getName() 时为完全初始化的 Container 对象。在第一种情况下,您会收到 NullPointerException,在第二种情况下,您会收到正确初始化的值。我认为这与 JCIP 引用的句子无关。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-05
      • 2013-05-31
      • 2020-11-29
      • 1970-01-01
      • 2018-02-28
      • 2022-12-18
      相关资源
      最近更新 更多