How deep volatile publication guarantees?
Asked Answered
U

1

8

As is known guarant that if we have some object reference and this reference has final field - we will see all reachable fields from final field(at least when constructor was finished)

example 1:

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

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

As I undertand at this case we have guarantee that bar() method always output object because:
1. I listed full code of class Foo and map is final;
2. If some thread will see reference of Foo and this reference != null, then we have guarantees that reachable from final map reference value will be actual.

also I think that

Example 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));
    }
}

here we have same gurantees about bar() method but bar2 can throw NullPointerException despite nonFinalMap assignment occurs before map assignment.

I want to know how about volatile:

Example 3:

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

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

As I understand bar() method cannot throw NullPoinerException but it can print null; (I am fully not sure about this aspect)

Example 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));
    }
}

I think here we have same gurantees about bar() method also bar2() cannot throw NullPointerException because nonVolatileMap assignment written higher volatile map assignment but it can output null


Added after Elliott Frisch comment

Publication through race example:

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();

    }
}

Please proove or correct my comments to code snippets.

Untie answered 2/2, 2017 at 21:31 Comment(4)
No caller can access the map (and there are no methods to write to the map) so I'm confused as to why you are concerned about threads.Evocator
@Elliott Frisch which example are you discuss? also read following:#41955848 and #41984225Untie
@user889742 Thanks, fixed. looks like I am tiredUntie
@Elliott Frisch I have added example of race publicationUntie
E
23

In the realm of current Java Memory Model, volatile does not equal final. In other words, you cannot replace final with volatile, and think the safe construction guarantees are the same. Notably, this can theoretically happen:

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"

So, writing the volatile field in constructor is not as safe.

Intuition: there is a race on m in the example above. That race is not eliminated by making the field M.x volatile, only making the m itself volatile would help. In other words, volatile modifier in that example is at the wrong place to be useful. In safe publication, you have to have "writes -> volatile write -> volatile read that observes volatile write -> reads (now observing writes prior the volatile write)", and instead you have "volatile write -> write -> read -> volatile read (that does not observe the volatile write)".

Trivia 1: This property means we can optimize volatile-s much more aggressively in constructors. This corroborates the intuition that unobserved volatile store (and indeed it is not observed until constructor with non-escaping this finishes) can be relaxed.

Trivia 2: This also means you cannot safely initialize volatile variables. Replace M with AtomicInteger in the example above, and you have a peculiar real-life behavior! Call new AtomicInteger(42) in one thread, publish the instance unsafely, and do get() in another thread -- are you guaranteed to observe 42? JMM, as stated, says "nope". Newer revisions of Java Memory Model strive to guarantee safe construction for all initializations, to capture this case. And many non-x86 ports where that matters have already strengthened this to be safe.

Trivia 3: Doug Lea: "This final vs volatile issue has led to some twisty constructions in java.util.concurrent to allow 0 as the base/default value in cases where it would not naturally be. This rule sucks and should be changed."

That said, the example can be made more cunning:

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"

If there is a transitive read through volatile field after volatile read observed the value written by volatile write in constructor, the usual safe publication rules kick in.

Entire answered 10/2, 2017 at 8:51 Comment(10)
almost clear for me. But I didn't catch Trivia 2 and Which behaviour would be in case of AtomicInteger usageUntie
Call new AtomicInteger(42) in one thread, publish it unsafely, and do get() in another thread -- are you guaranteed to observe 42? JMM, as stated, says "nope".Entire
because AtomicInteger just wrapper over volatile int ?Untie
Yes. See the symmetry between AtomicInteger and M from the example.Entire
Just to clarify - in your first example, even if we had final int x; instead of volatile, are we guaranteed to see 42 in thread 2? By my knowledge, I would say no. What confuses me is the m variable - if it is not volatile, the while loop might loop forever. Or we could see a partially constructed M instance, could we not? JCIP taught me that one visibility error spreads all over you if you try to just roll with it...Selena
@Petr Janeček I am fully sure that in case of final in first example you have guaratees to see 42Untie
@Petr Janeček if you see non nul reference, you could be sure that object (where reference points) final fields have initialized already and you wiil see it from any threadUntie
Yeah, changing volatile to final precludes 0 as the result. That's the whole reason for final-s to exist: to survive data races.Entire
@PetrJaneček: "What confuses me is the m variable - if it is not volatile, the while loop might loop forever." Oh, OPs example had a loop, and I tried to match that. But this is actually a teachable moment: the fact that compilers may reduce non-volatile loop into the infinite one, does not mean they should do it. JMM only says what results can a compliant implementation print out in that example: for non-volatile loop, it says {nothing, 0, 42}.Entire
@Aleksey, please help me to answer #42364506Untie

© 2022 - 2024 — McMap. All rights reserved.