不稳定的出版物保证多深?

时间:2017-02-02 21:31:31

标签: java concurrency visibility volatile happens-before

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

示例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分配。

我想知道如何挥发:

示例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赋值写了更高的volatile map赋值但它可以输出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();

    }
}

请将我的评论改为或更正为代码段。

1 个答案:

答案 0 :(得分:22)

在当前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上有一场比赛。通过制作字段 M.x volatile不会消除这种竞争,只会使m本身volatile有所帮助。换句话说,该示例中的volatile修饰符位于错误位置有用。在安全发布中,您必须具有“写入 - >易失性写入 - >易失性读取,观察易失性写入 - >读取(现在观察易失性写入之前的写入)”,而是“易失性写入 - >写入” - > read - > volatile read(不遵守volatile写入)“。

琐事1:此属性意味着我们可以在构造函数中更积极地优化volatile。这证实了直觉,即未观察到的易失性存储(实际上,直到具有非逃逸this完成的构造函数才会被忽略)。

琐事2:这也意味着您无法安全地初始化volatile个变量。在上面的示例中将M替换为AtomicInteger,您就会有一种特殊的现实生活行为!在一个线程中调用new AtomicInteger(42),不安全地发布实例,在另一个线程中执行get() - 您是否可以保证观察42?如上所述,JMM说“不”。 Java内存模型的较新版本力求保证所有初始化的安全构建,以捕获此案例。许多非x86端口have already strengthened this是安全的。

琐事3: Doug Lea:“此final vs volatile问题导致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字段进行传递读取,则会在构造函数中观察由volatile写入的值,通常会发布安全发布规则。