Java concurrency interview conversation about passing shared variables to threads

During one interview, I got a pretty nice Java concurrency interview question. It was more or less a conversation between me and the interviewer about how Java handles Threads and passing references, plus ideas on solving concurrency pitfalls.

Let’s take a while and simulate such a conversation so you can be prepared for your next Java concurrency interview.

The whole ping-pong between interviewer and candidate is to test the candidate’s ability and knowledge of Java concurrency and way of handling access to shared variables.

Two threads with the same primitive int as argument

So, the narrative of the conversation goes like this:

Create two Java Threads. Each Thread takes the same single primitive int as an argument to its constructor. Let the first thread increment primitive int to 100 million. Let the second Thread increment the same shared variable to 20 million. How will look the output of both threads at the end of the incrementation?

So as you might guess already, this example will go in the way of figuring out if incrementing primitive int will be concurrent safe. What is meant by that, it incrementing the primitive int in one Thread will not influence the shared variable in the other Thread and vice versa.

Should both threads print 20 million, 100 million or one print count of 20 million and the second 100 million?

public class Main {
    public static void main(String[] args) {
        int sharedInt = 0;
        TOne tOne = new TOne(sharedInt);
        TTwo tTwo = new TTwo(sharedInt);
        tOne.start();
        tTwo.start();
    }
}

class TOne extends Thread {
    private int sharedInt;

    public TOne(int sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 100_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tOne: The shared primitive int is: " + this.sharedInt);
    }
}

class TTwo extends Thread {
    private int sharedInt;

    public TTwo(int sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 20_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tTwo: The shared primitive int is: " + this.sharedInt);
    }
}

Let's put the code to the file name Main.java, compile it and run it. As you can see, the tTwo execute immediately as it increments much lower than tOne. Both threads show the results as expected, although tTwo finished before tOne, which does not look correct.

tTwo: The shared primitive int is: 20000000
tOne: The shared primitive int is: 100000000

Would there be a way for tTwo to wait for tOne? What will be the output? We still do not know if they share the variable sharedInt of primitive int, if to tTwo takes a shorter time to execute as to tOne.

One Thread waits for another one to finish

We need to adjust our code slightly. We need to use Thread class join() method.

The whole premise of calling join() method upon calling Thread is for calling Thread to stop and go to a waiting state. Calling Thread will remain to wait until the referenced Thread upon which the join() method was called terminates. This way we block tTwo before executing.

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int sharedInt = 0;
        TOne tOne = new TOne(sharedInt);
        TTwo tTwo = new TTwo(sharedInt);
        tOne.start();
        tOne.join();
        tTwo.start();
    }
}

class TOne extends Thread {
    private int sharedInt;

    public TOne(int sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 100_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tOne: The shared primitive int is: " + this.sharedInt);
    }
}

class TTwo extends Thread {
    private int sharedInt;

    public TTwo(int sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 20_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tTwo: The shared primitive int is: " + this.sharedInt);
    }
}

Let's compile and run the code above:

tOne: The shared primitive int is: 100000000
tTwo: The shared primitive int is: 20000000

When we run the code, we conclude that even though the variable is shared, it is isolated in the threads. The reason is that each Java Thread on its creation allocates and creates resources. And primitive types make their copies; they do not pass as a reference on the new threads. However, will this rule apply to object-type variables?

Two threads with the same Integer variable as argument

But what if we will use Object variable instead primitive int type? Will it be the same? Will the instance of the object also be isolated?

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Integer sharedInt = 0;
        TOne tOne = new TOne(sharedInt);
        TTwo tTwo = new TTwo(sharedInt);
        tOne.start();
        tTwo.start();
    }
}

class TOne extends Thread {
    private Integer sharedInt;

    public TOne(Integer sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 100_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tOne: The shared object Integer is: " + this.sharedInt);
    }
}

class TTwo extends Thread {
    private Integer sharedInt;

    public TTwo(Integer sharedInt) {
        this.sharedInt = sharedInt;
    }

    public void run() {
        for (int i = 0; i < 20_000_000; i++) {
            this.sharedInt += 1;
        }
        System.out.println("tTwo: The shared object Integer is: " + this.sharedInt);
    }
}

The output of the code above is surprisingly correct:

tTwo: The shared object Integer is: 20000000
tOne: The shared object Integer is: 100000000

Both threads are doing relatively well. However, with a close inspection under the hood, we find out something fishy is happening. If we look better at the assigned references to sharedInt, both threads have assigned the same reference before the thread executions. However, after the run, the sharedInt in tOne had assigned a different reference. Thus, while the output is correct, each Thread's referenced object changes. Therefore, each Thread on every iteration assigns a new object to sharedInt. Consequently, at the end of execution, the different object is printed.

Rather than using the object variable of Integer class, let's make a wrapping class around the count so the passed object variable will not change. What will change will be its internal state. Here is a code with Wrapper class:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Wrapper wrapper = new Wrapper(0);
        TOne tOne = new TOne(wrapper);
        TTwo tTwo = new TTwo(wrapper);
        tOne.start();
        tTwo.start();
    }
}

class TOne extends Thread {
    private Wrapper wrapper;

    public TOne(Wrapper wrapper) {
        this.wrapper = wrapper;
    }

    public void run() {
        synchronize {
            for (int i = 0; i < 100_000_000; i++) {
                this.wrapper.increment();
            }
            System.out.println("tOne: Count is " + this.wrapper.printCount());
        }
    }
}

class TTwo extends Thread {
    private Wrapper wrapper;

    public TTwo(Wrapper wrapper) {
        this.wrapper = wrapper;
    }

    public void run() {
        synchronize {
            for (int i = 0; i < 20_000_000; i++) {
                this.wrapper.increment();
            }
            System.out.println("tTwo: Count is " + this.wrapper.printCount());
        }
    }
}

class Wrapper {
    private Integer count;

    public Wrapper(Integer count) {
        this.count = count;
    }

    public void increment() {
        this.count++;
    }

    public Integer printCount() {
        return this.count;
    }
}

Result is shocking! More and more you try, you will get slightly different numbers every time. The result is not 20 million, nor 100, or 120. Why?! As you can see, there is clearly a problem with synchronization and keeping the isolated internal state of the wrapper from one Thread to another.

tTwo: Count is 22095354
tOne: Count is 104408141

The whole concurrency is about keeping the data isolated from one Thread to another. We ended the interview section as the conversation showed several vital elements of your concurrency knowledge. You successfully demonstrated mastery of the difference between passing reference for primitive and object variables, ways for synchronizing threads and how to show and estimate concurrent output.

The interview might continue, and the interviewer might challenge you to fix the issues. So formalize the problem with the current solution and figure out how to fix the current situation and isolate the internal state of the wrapper.

Passing object and isolating its internal state

How can we isolate the internal state? How to secure the synchronization of internal variables?

The question is now, thus, how we can fix the current situation. There are several mechanisms in Java to keep the inner state concurrent. It would be worth telling about each of them in a separate article. But you might start discussing each Java concurrency mechanism with the interviewer, how it can fix the code, and what the output will be.

Let's show one way to fix the issue, which will be through the use of Locks.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Wrapper wrapper = new Wrapper(0);
        TOne tOne = new TOne(wrapper);
        TTwo tTwo = new TTwo(wrapper);
        tOne.start();
        tTwo.start();
        // tOne.join();
        // tTwo.join();
        System.out.println("Final count: " + wrapper.printCount());
    }
}

class TOne extends Thread {
    private Wrapper wrapper;

    public TOne(Wrapper wrapper) {
        this.wrapper = wrapper;
    }

    public void run() {
        this.wrapper.getLock();
        try {
            for (int i = 0; i < 100_000_000; i++) {
                this.wrapper.increment();
            }
        } finally {
            this.wrapper.unlock();
        }
        System.out.println("tOne: Count is " + this.wrapper.printCount());
    }
}

class TTwo extends Thread {
    private Wrapper wrapper;

    public TTwo(Wrapper wrapper) {
        this.wrapper = wrapper;
    }

    public void run() {
        this.wrapper.getLock();
        try {
            for (int i = 0; i < 20_000_000; i++) {
                this.wrapper.increment();
            }
        } finally {
            this.wrapper.unlock();
        }
        System.out.println("tTwo: Count is " + this.wrapper.printCount());
    }
}

class Wrapper {
    private Integer count;
    private Lock lock;

    public Wrapper(Integer count) {
        this.count = count;
        this.lock = new ReentrantLock();
    }

    public void getLock() {
        lock.lock();
    }

    public void unlock() {
        lock.unlock();
    }

    public void increment() {
        this.count++;
    }

    public Integer printCount() {
        return this.count;
    }
}

If you choose not to use join() methods for both threads, the main Java thread will execute first and will leave the two other threads running in parallel. The output will be like this:

Final count: 0
tOne: Count is 100000000
tTwo: Count is 120000000

However, if you decide to use join() method, the output will be as follows:

tOne: Count is 100000000
tTwo: Count is 120000000
Final count: 120000000

Conclusion

As said before, the whole concept of concurrency is about keeping the data isolated from one Thread to another. You can use this conversation to prepare for an interview for your understanding of concurrent mechanisms. But the successful demonstration of concepts demonstrated your knowledge of the difference between passing reference for primitive variables and object variables, ways for synchronizing threads and how to show and estimate concurrent output.

Don't come only with answers to the interviewers' questions. Try to come also with solutions to problems and pitfalls of concurrency.

And for interviewers, there are several alternatives of how the interview conversation might go between you and the candidate. Suppose this conversation about concurrency inspires you. In that case, you might challenge the candidate to come up with different solutions, can for example, obey using a wrapper and instead focus on handling the situation with volatile or synchronization keyword.

This entry was posted in Java Concurrency and tagged , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.