多深的易失性出版物保证?

Posted

技术标签:

【中文标题】多深的易失性出版物保证?【英文标题】:How deep volatile publication guarantees? 【发布时间】:2017-06-20 03:22:44 【问题描述】:

众所周知,如果我们有一些对象引用并且该引用具有最终字段 - 我们将看到来自最终字段的所有可访问字段(至少在构造函数完成时)

示例 1:

class Foo
    private final Map map;
     Foo()
         map = new HashMap();
         map.put(1,"object");
     

     public void bar()
       System.out.println(map.get(1));
     

据我所知,在这种情况下,我们保证bar() 方法始终输出object,因为: 1. 我列出了类Foo 的完整代码,并且地图是最终的;2.如果某个线程会看到Foo 的引用并且这个引用!= null,那么我们保证从最终的map 引用值可以到达是实际的

我也觉得

示例 2:

class Foo 
    private final Map map;
    private Map nonFinalMap;

    Foo() 
        nonFinalMap = new HashMap();
        nonFinalMap.put(2, "ololo");
        map = new HashMap();
        map.put(1, "object");
    

    public void bar() 
        System.out.println(map.get(1));
    

    public void bar2() 
        System.out.println(nonFinalMap.get(2));
    

在这里,我们对bar() 方法有相同的保证,但bar2 可以抛出NullPointerException,尽管nonFinalMap 分配发生在map 分配之前。

我想知道 volatile 怎么样:

示例 3:

class Foo
        private volatile Map map;
         Foo()
             map = new HashMap();
             map.put(1,"object");
         

         public void bar()
           System.out.println(map.get(1));
         
    

据我了解bar() 方法不能抛出NullPoinerException 但它可以打印null; (我完全不确定这方面)

示例 4:

class Foo 
    private volatile Map map;
    private Map nonVolatileMap;

    Foo() 
        nonVolatileMap= new HashMap();
        nonVolatileMap.put(2, "ololo");
        map = new HashMap();
        map.put(1, "object");
    

    public void bar() 
        System.out.println(map.get(1));
    

    public void bar2() 
        System.out.println(nonFinalMap.get(2));
    

我认为这里我们对 bar() 方法也有相同的保证 bar2() 不能抛出 NullPointerException 因为 nonVolatileMap assignment 写了更高的 volatile map assignment 但它可以输出 null


在 Elliott Frisch 评论后添加

通过比赛示例发布:

public class Main 
    private static Foo foo;

    public static void main(String[] args) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                foo = new Foo();
            
        ).start();


        new Thread(new Runnable() 
            @Override
            public void run() 
                while (foo == null) ; // empty loop

                foo.bar();
            
        ).start();

    

请证明或更正我的 cmets 以编写 sn-ps。

【问题讨论】:

没有调用者可以访问map(也没有写入map的方法)所以我很困惑你为什么关心线程。 @Elliott Frisch 你在讨论哪个例子?另请阅读以下内容:***.com/questions/41955348/… 和 ***.com/questions/41983725/… @user889742 谢谢,已修复。看来我累了 @Elliott Frisch 我添加了种族发布的示例 【参考方案1】:

在当前 Java 内存模型领域,volatile 不等于 final。换句话说,you cannot replace final with volatile,和认为安全施工的保证是一样的。值得注意的是,这在理论上是可以发生的:

public class M 
  volatile int x;
  M(int v)  this.x = v; 
  int x()  return x; 


// thread 1
m = new M(42);

// thread 2
M lm;
while ((lm = m) == null); // wait for it
print(lm.x()); // allowed to print "0"

因此,在构造函数中写入volatile 字段并不安全。

直觉:在上面的示例中,m 上有一场比赛。使 field M.x volatile 并不能消除这场比赛,只有使 m 本身 volatile 会有所帮助。换句话说,该示例中的volatile 修饰符在错误的地方 是有用的。在安全发布中,您必须具有“写入 -> 易失性写入 -> 易失性读取,观察易失性写入 -> 读取(现在观察易失性写入之前的写入)”,而不是“易失性写入 -> 写入 -> 读取 - > 易失性读取(即不观察易失性写入)”。

琐事1:这个属性意味着我们可以在构造函数中更积极地优化volatile-s。这证实了一种直觉,即未观察到的 volatile 存储(实际上直到具有非转义 this 的构造函数完成后才观察到)可以放松。

琐事 2: 这也意味着您无法安全地初始化 volatile 变量。在上面的例子中,将M 替换为AtomicInteger,你就有了一种奇特的现实行为!在一个线程中调用new AtomicInteger(42),不安全地发布实例,并在另一个线程中执行get()——你能保证观察到42吗?如前所述,JMM 说“不”。 Java 内存模型的较新版本努力保证所有初始化的安全构造,以捕捉这种情况。还有许多非 x86 端口,这对 have already strengthened this 的安全很重要。

琐事 3: Doug Lea:“这个 finalvolatile 问题导致 java.util.concurrent 中的一些曲折的结构允许 0 作为基本/默认值它不会自然存在。这条规则很糟糕,应该改变。”

也就是说,这个例子可以做得更狡猾:

public class C 
  int v;
  C(int v)  this.x = v; 
  int x()  return x;     


public class M 
  volatile C c;
  M(int v)  this.c = new C(v); 
  int x()  
    while (c == null); // wait!
    return c.x();
  


// thread 1
m = new M(42);

// thread 2
M lm;
while ((lm = m) == null); // wait for it
print(lm.x()); // always prints "42"

如果通过volatile 字段进行传递读取 volatile read 观察到构造函数中的 volatile write 写入的值,通常的安全发布规则就会启动。

【讨论】:

对我来说几乎很清楚。但我没有抓住 Trivia 2 以及在使用 AtomicInteger 的情况下会出现哪种行为 在一个线程中调用new AtomicInteger(42),不安全地发布它,然后在另一个线程中执行get()——你能保证观察到42吗?如前所述,JMM 说“不”。 因为 AtomicInteger 只是包装在 volatile int 上? 是的。参见示例中AtomicIntegerM 之间的对称性。 @PetrJaneček:“让我感到困惑的是 m 变量 - 如果它不是易失性的,那么 while 循环可能会永远循环。”哦,OP 示例有一个循环,我试图匹配它。但这实际上是一个可教的时刻:编译器可能将非易失性循环减少为无限循环这一事实,并不意味着他们应该这样做。 JMM 仅说明在该示例中兼容的实现可以打印出什么结果:对于非易失性循环,它说 nothing, 0, 42。

以上是关于多深的易失性出版物保证?的主要内容,如果未能解决你的问题,请参考以下文章

优化volatile变量

基于NAND闪存的SSD解决方案的STT-MRAM

基于NAND闪存的SSD解决方案的STT-MRAM

将易失性数组与非易失性数组进行比较

将易失性数组转换为非易失性数组

什么是STT-MRAM?