Search code examples
javamultithreadingvolatile

Why doesn't volatile in Java update the value of a variable?


I've read that "volatile" in Java allows different threads to have access to the same field and see changes the other threads has made to that field. If that's the case, I'd predict that when the first and second thread have completely run, the value of "d" will be incremented to 4. But instead, each thread increments "d" to a value of 2.

public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0; 

    public VolatileExample(String name) {
        super(name);
        start();
    }

    public String toString() {
       return super.getName() + ": countDown " + countDown;
    }

public void run() {
        while(true) {
        d = d + 1;
    System.out.println(this + ". Value of d is " + d);
    if(--countDown == 0) return;
    }
}

public static void main(String[] args) {
    new VolatileExample("first thread");
    new VolatileExample("second thread");
    }
}

The results from running this program are:

first thread: countDown 2. Value of d is 1

second thread: countDown 2. Value of d is 1

first thread: countDown 1. Value of d is 2

second thread: countDown 1. Value of d is 2

I understand that if I add keyword "static" the program, (that is, "private static volatile int d = 0;"), "d" would be incremented to 4. And I know that's because d will become a variable that the whole class shares rather than each instance getting a copy.

The results look like:

first thread: countDown 2. Value of d is 1

first thread: countDown 1. Value of d is 3

second thread: countDown 2. Value of d is 2

second thread: countDown 1. Value of d is 4

My question is, why doesn't "private volatile int d = 0; " yield similar results if volatile is supposed to allow the sharing of "d" between the two threads? That is, if the first thread updates the value of d to 1, then why doesn't the second thread grab the value of d as 1 rather than as zero?


Solution

  • There are a couple of misunderstandings here. You seem not to properly understand what a thread is, what an instance field is and what a static field is.

    An instance field is a memory location that gets allocated once you instantiate a class (ie, a memory location gets allocated for a field d when you VolatileExample v = new VolatileExample()). To reference that memory location from within the class, you do this.d (then you can write to and read from that memory location). To reference that memory location from outside the class, it must be acessible (ie, not private), and then you'd do v.d. As you can see, each instance of a class gets its own memory location for its own field d. So, if you have 2 different instances of VolatileExample, each will have its own, independent, field d.

    A static field is a memory location that gets allocated once a class is initialized (which, forgetting about the possibility of using multiple ClassLoaders, happens exactly once). So, you can think that a static field is some kind of global variable. To reference that memory location, you'd use VolatileExample.d (accessibility also applies (ie, if it is private, it can only be done from within the class)).

    Finally, a thread of execution is a sequence of steps that will be executed by the JVM. You must not think of a thread as a class, or an instance of the class Thread, it will only get you confused. It is as simple as that: a sequence of steps.

    The main sequence of steps is what is defined in the main(...) method. It is that sequence of steps that the JVM will start executing when you launch your program.

    If you want to start a new thread of execution to run simultaneously (ie, you want a separate sequence of steps to be run concurrently), in Java you do so by creating an instance of the class Thread and calling its start() method.

    Let's modify your code a little bit so that it is easier to understand what is happening:

    public class VolatileExample extends Thread {
      private int countDown = 2;
      private volatile int d = 0;
    
      public VolatileExample(String name) {
        super(name);
      }
    
      public String toString() {
        return super.getName() + ": countDown " + countDown;
      }
    
      public void run() {
        while(true) {
          d = d + 1;
          System.out.println(this + ". Value of d is " + d);
          if(--countDown == 0) return;
        }
      }
    
      public static void main(String[] args) {
        VolatileExample ve1 = new VolatileExample("first thread");
        ve1.start();
        VolatileExample ve2 = new VolatileExample("second thread");
        ve2.start();
      }
    }
    

    The line VolatileExample ve1 = new VolatileExample("first thread"); creates an instance of VolatileExample. This will allocate some memory locations: 4 bytes for countdown and 4 bytes for d. Then you start a new thread of execution: ve1.start();. This thread of execution will access (read from and write to) the memory locations described before in this paragraph.

    The next line, VolatileExample ve2 = new VolatileExample("second thread"); creates another instance of VolatileExample, which will allocate 2 new memory locations: 4 bytes for ve2's countdown and 4 bytes for ve2's d. Then, you start a thread of execution, which will access THESE NEW memory locations, and not those described in the paragraph before this one.

    Now, with or without volatile, you see that you have two different fields d : each thread operates on a different field. Therefore, it is unreasonable for you to expect that d would get incremented to 4, since there's no single d.

    If you make d a static field, only then both threads would (supposedly) be operating on the same memory location. Only then volatile would come into play, since only then you'd be sharing a memory location between different threads.

    If you make a field volatile, you are guaranteed that writes go straight to the main memory and reads come straight from the main memory (ie, they won't get cached on some -- extremely fast -- processor-local cache, the operations would take longer but would be guaranteed to be visible to other threads).

    It wouldn't, however, guarantee that you'd see the value 4 stored on d. That's because volatile solves visibility problem, but not atomicity problems: increment = read from main memory + operation on the value + write to main memory. As you can see, 2 different threads could read the initial value (0), operate (locally) on it (obtaining 1), then writing it to the main memory (both would end up writing 1) -- the 2 increments would be perceived as only 1.

    To solve this, you must make the increment an atomic operation. To do so, you'd need to either use a synchronization mechanism -- a mutex (synchronized (...) { ... }, or an explicit lock) -- or a class designed specifically for this things: AtomicInteger.