8

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

示例 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. 如果某个线程会看到引用 ofFoo并且这个引用 != 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可以抛出 。NullPointerExceptionnonFinalMapmap

我想知道 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赋值写了更高的易失映射赋值,但它可以输出 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();

    }
}

请证明或更正我对代码片段的评论。

4

1 回答 1

23

在当前 Java 内存模型领域,volatile不等于final. 换句话说,您不能替换finalvolatile,并且认为安全构造保证是相同的。值得注意的是,这在理论上可能会发生:

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该示例中的修饰符在错误的地方有用。在安全发布中,您必须具有“写入 -> 易失性写入 -> 易失性读取,观察易失性写入 -> 读取(现在观察易失性写入之前的写入)”,而不是“易失性写入 -> 写入 -> 读取 - > 易失性读取(即不观察易失性写入)”。

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

琐事 2:这也意味着您不能安全地初始化volatile变量。在上面的示例中替换MAtomicInteger,您将拥有一种特殊的现实生活行为!在一个线程中调用new AtomicInteger(42),不安全地发布实例,然后get()在另一个线程中执行——你保证观察到42吗?如前所述,JMM 说“不”。Java 内存模型的较新版本努力保证所有初始化的安全构造,以捕捉这种情况。许多非 x86 端口已经加强了这一点以确保安全。

琐事 3: Doug Lea:“这个finalvsvolatile问题导致了 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 read 观察到构造函数中的 volatile write 写入的值之后存在传递性读取volatile字段,则通常的安全发布规则会启动。

于 2017-02-10T08:51:52.683 回答