Home > Software design >  Trying to understand shared variables in java threads
Trying to understand shared variables in java threads

Time:03-28

I have the following code :

class thread_creation extends Thread{
    int t;
    thread_creation(int x){
        t=x;
    }
    
    public void run() {
    increment();
        
        
    }
    
    public void increment() {
        for(int i =0 ; i<10 ; i  ) {
            t  ;
    System.out.println(t);
        }
        
    }
}




public class test {

    public static void main(String[] args) {
        int i =0;
        thread_creation t1 = new thread_creation(i);
        thread_creation t2 = new thread_creation(i);
        t1.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        t2.start();
    }

}

When I run it , I get :

1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10

Why I am getting this output ? According to my understanding , the variable i is a shared variable between the two threads created. So according to the code , the first thread will execute and increments i 10 times , and hence , i will be equal to 10 . The second thread will start after the first one because of the sleep statement and since i is shared , then the second thread will start will i=10 and will start incrementing it 10 times to have i = 20 , but this is not the case in the output , so why that ?

CodePudding user response:

Two crucial things you're missing. Both individually explain this behaviour - you can 'fix' either one and you'll still see this, you'd have to fix both to see 1-20:

Java is pass-by-value

When you pass i, you pass a copy of it. In fact, in java, all parameters to methods are always copies. Hence, when the thread does t , it has absolutely no effect whatsoever on your i. You can trivially test this, and you don't need to mess with threads to see it:

public static void main(String[] args) {
  int i = 0;
  add5(i);
  System.out.println(i); // prints 0!!
}

static void add5(int i) {
  i = i   5;
}

Note that all non-primitives are references. That means: A copy of the reference is passed. It's like passing the address of a house and not the house itself. If I have an address book, and I hand you a scanned copy of a page that contains the address to my summer home, you can still drive over there and toss a brick through the window, and I'll 'see' that when I go follow my copy of the address. So, when you pass e.g. a list and the method you passed the list to runs list.add("foo"), you DO see that. You may think: AHA! That means java does not pass a copy, it passed the real list! Not so. Java passed a copy of a street address (A reference). The method I handed that copy to decided to drive over there and act - that you can see.

In other words, =, , that sort of thing? That is done to the copy. . is java for 'drive to the address and enter the house'. Anything you 'do' with . is visible to the caller, = and and such are not.

Fixing the code to avoid the pass-by-value problem

Change your code to:

class thread_creation extends Thread {
    static int t; // now its global!
    
    public void run() {
      increment();
    }
    
    public void increment() {
        for(int i =0 ; i<10 ; i  ) {
            t  ;
//            System.out.println(t);
        }
    }
}

public class test {
    public static void main(String[] args) throws Exception {
        thread_creation t1 = new thread_creation();
        thread_creation t2 = new thread_creation();
        t1.start();
        Thread.sleep(500);
        t2.start();
        Thread.sleep(500);
        System.out.println(thread_creation.t);
    }
}

Note that I remarked out the print line. I did that intentionally - see below. If you run the above code, you'd think you see 20, but depending on your hardware, the OS, the song playing on your mp3 playing app, which websites you have open, and the phase of the moon, it may be less than 20. So what's going on there? Enter the...

The evil coin.

The relevant spec here is the JMM (The Java Memory Model). This spec explains precisely what a JVM must do, and therefore, what a JVM is free not to do, especially when it comes to how memory is actually managed.

The crucial aspect is the following:

  • Any effects (updates to fields, such as that t field) may or may not be observable, JVM's choice. There's no guarantee that anything you do is visible to anything else... unless there exists a Happens-Before/Happens-After relationship: Any 2 statements with such a relationship have the property that the JVM guarantees that you cannot observe the lack of the update done by the HB line from the HA line.

HB/HA can be established in various ways:

  • The 'natural' way: Anything that is 'before' something else _and runs in the same thread has an HB/HA relationship. In other words, if you do in one thread x ; System.out.println(x); then you can't observe that the x hasn't happened yet. It's stated like this so that if you're not observing, you get no guarantees, which gives the JVM the freedom to optimize. For example, Given x ;y ; and that's all you do, the JVM is free to re-order that and increment y before x. Or not. There are no guarantees, a JVM can do whatever it wants.

  • synchronized. The moment of 'exiting' a synchronized (x) {} block has HB to the HA of another thread 'entering' the top of any synchronized block on the same object, if it enters later.

  • volatile - but note that with volatile it's basically impossible which one came first. But one of them did, and any interaction with a volatile field is HB relative to another thread accessing the same field later.

  • thread starting. thread.start() is HB relative to the first line of the run() of that thread.

  • thread yielding. thread.yield() is HA relative to the last line of the thread.

There are a few more exotic ways to establish HB/HA but that's pretty much it.

Crucially, in your code there is no HB/HA between any of the statements that modify or print t!

In other words, the JVM is free to run it all in such a way that the effects of various t statements run by one thread aren't observed by another thread.

What the.. WHY????

Because of efficiency. Your memory banks on your CPU are, relative to how fast CPUs are, oceans away from the CPU core. Fetching or writing to core memory from a CPU takes an incredibly long time - your CPU is twiddling its thumbs for a very long time while it waits for the memory controller to get the job done. It could be running hundreds of instructions in that time.

So, CPU cores do not write to memory AT ALL. Instead they work with caches: They have an on-core cache page, and the only interaction with your main memory banks (which are shared by CPU cores) is 'load in an entire cache page' and 'write an entire cache page'. That cache page is then effectively a 'local copy' that only that core can see and interact with (but can do so very very quickly, as that IS very close to the core, unlike the main memory banks), and then once the algorithm is done it can flush that page back to main memory.

The JVM needs to be free to use this. Had the JVM actually worked like you want (that anything any thread does is instantly observable by all others), then anything that any line does must first wait 500 cycles to load the relevant page, then wait another 500 cycles to write it back. All java apps would literally be 1000x slower than they could be.

This in passing also explains that actual synchronizing is really slow. Nothing java can do about that, it is a fundamental limitation of our modern multi-core CPUs.

So, evil coin?

Note that the JVM does not guarantee that the CPU must neccessarily work with this cache stuff, nor does it make any promises about when cache pages are flushed. It merely limits the guarantees so that JVMs can be efficiently written on CPUs that work like that.

That means that any read or write to any field any java code ever does can best be thought of as follows:

The JVM first flips a coin. On heads, it uses a local cached copy. On tails, it copies over the value from some other thread's cached copy instead.

The coin is evil: It is not reliably a 50/50 arrangement. It is entirely plausible that throughout developing a feature and testing it, the coin lands tails every time it is flipped. It remains flipping tails 100% of the time for the first week that you deployed it. And then just when that big potential customer comes in and you're demoing your app, the coin, being an evil, evil coin, starts flipping heads a few times and breaking your app.

The correct conclusion is that the coin will mess with you and that you cannot unit test against it. The only way to win the game is to ensure that the coin is never flipped.

You do this by never touching a field from multiple threads unless it is constant (final, or simply never changes), or if all access to it (both reads and writes) has clearly established HB/HA between all threads.

This is hard to do. That's why the vast majority of apps don't do it at all. Instead, they:

  • Talk between threads using a database, which has vastly more advanced synchronization primitives: Transactions.
  • Talk using a message bus such as RabbitMQ or similar.
  • Use stuff from the java.util.concurrent package such as a Latch, ForkJoin, ConcurrentMap, or AtomicInteger. These are easier to use (specifically: It is a lot harder to write code for these abstractions that is buggy but where the bug cannot be observed or tested for on the machine of the developer that wrote it, it'll only blow up much later in production. But not impossible, of course).

Let's fix it!

volatile doesn't 'fix' . x ; is 'read x, increment by 1, write result to x' and volatile doesn't make that atomic, so we cannot use this. We can either replace t with:

synchronized(thread_creation.class) {
  t  ;
}

Which works fine but is really slow (and you shouldn't lock on publicly visible stuff if you can help it, so make a custom object to lock on, but you get the gist hopefully), or, better, dig into that j.u.c package for something that seems useful. And so there is! AtomicInteger!

class thread_creation extends Thread {
    static AtomicInteger t = new AtomicInteger();
    
    public void run() {
      increment();
    }
    
    public void increment() {
        for(int i =0 ; i<10 ; i  ) {
            t.incrementAndGet();
        }
    }
}

public class test {
    public static void main(String[] args) throws Exception {
        thread_creation t1 = new thread_creation();
        thread_creation t2 = new thread_creation();
        t1.start();
        Thread.sleep(500);
        t2.start();
        Thread.sleep(500);
        System.out.println(thread_creation.t.get());
    }
}

That code will print 20. Every time (unless those threads take longer than 500msec which technically could be, but is rather unlikely of course).

Why did you remark out the print statement?

That HB/HA stuff can sneak up on you: When you call code you did not write, such as System.out.println, who knows what kind of HB/HA relationships are in that code? Javadoc isn't that kind of specific, they won't tell you. Turns out that on most OSes and JVM implementations, interaction with standard out, such as System.out.println, causes synchronization; either the JVM does it, or the OS does. Thus, introducing print statements 'to test stuff' doesn't work - that makes it impossible to observe the race conditions your code does have. Similarly, involving debuggers is a great way to make that coin really go evil on you and flip juuust so that you can't tell your code is buggy.

That is why I remarked it out, because with it in, I bet on almost all hardware you end up seeing 20 eventhough the JVM doesn't guarantee it and that first version is broken. Even if on your particular machine, on this day, with this phase of the moon, it seems to reliably print 20 every single time you run it.

CodePudding user response:

You seem to think that int t; in thread_creation is a shared variable. I'm afraid you are mistaken. Each t instance is a different variable. So the two threads are updating distinct counters.

The output you are seeing reflects that.


This is the nub of your question:

How do I pass a shared variable then ?

Actually, you can't1. Strictly a shared variable is actually a variable of a shared object. You cannot pass a variable per se. Java does not allow passing of variables. This is what "Java does not support call-by-reference" really means. You can't pass or return a variable or the address of a variable in any method call. (Or any other way.)


So the way the simple implement a shared counter is to implement it as a shared counter object.

(Note that "variable" and "object" mean different things. You shouldn't use the two terms interchangeable; e.g. when I declare

 String s = "Hello";

the s variable is not a String object. It is a variable that contains a reference to the String object. Other variables may contain references to the same String object as well.)

Here are the two best ways to do it.

  • You could create a custom Java Counter class with a count variable, a get method, and methods for incrementing, decrementing the counter. The class needs to implement various methods as thread-safe and atomic; e.g. by using synchronized methods or blocks2.

  • You could just use an AtomicInteger instance. That takes care of atomicity and thread-safety ... to the extent that it is possible with this kind of API.

The latter approach is simpler and most likely more efficient ... unless you need to do something special each time the counter changes.

(It is conceivable that you could implement a shared counter other ways, but that is too much detail for this answer.)


1 - I realize that I just said the same thing more than 3 times. But as the Bellman says in "The Hunting of the Snark": "What I tell you three times is true."
2 - If the counter is not implemented using synchronized or an equivalent mutual exclusion mechanism with the appropriate happens before semantics, you are liable to see Heisenbugs; e.g. race conditions and memory visibility problems.

  • Related