Sharing data between Java threads

October 15, 2020

The Java memory model describes where variables and objects are stored (stack or heap) and how Java threads can have access to them.

As you problably know, reading and writing from/to the same memory area from multiple threads of execution can cause problems due to race conditions.
Data integrity cannot be preserved without proper protection/“synchronized access”/“mutual exclusion” to the shared memory/“critical section”.

For example, a thread might read a variable before it is modified by another thread
and then make its own modification without knowing that the other thread modified it already to some other value.
This situation can lead to very unexpected results if not properly taken care of.

In Java there are a few ways to solve this data integrity problem,
i.e. only one thread should read or read&write the shared data at a time.

The basic concurrency primitives are: synchronized blocks or volatile variables.

The more advanced features can be found in the java.util.concurrent package.
Some examples of useful classes:

  • Lock
  • Semaphore
  • CountDownLatch
  • ReadWriteLock
  • AtomicInteger
  • AtomicLong
  • ExecutorService
  • ThreadPoolExecutor
  • BlockingQueue
  • ConcurrentMap

But first let’s understand what is the problem with shared data
and what are the ways in which data can be shared between multiple threads.

Shared Runnable instance between multiple threads

package dev.bitek.multithreading.example13;

public class ThreadExample13 {
    public static class MyTask implements Runnable {

        // The count member variable is shared between multiple threads
        // that are executing the same instance of the MyTask runnable.
        private int count = 0;

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();

            // object is a reference stored in the thread stack of the current thread
            // that is pointing to an object allocated in the heap area
            // and a new instance of the object is created in each thread
            Object object = new Object();
            System.out.printf("[%s] %s\n", threadName, object);

            // i is a local variable created on the thread stack of the current thread
            for (int i = 0; i < 42_000; i++) {
                this.count++;
            }

            System.out.printf("[%s] Count: %d\n", threadName, this.count);
        }
    }
    public static void main(String[] args) {
        Runnable myTask = new MyTask();

        Thread thread1 = new Thread(myTask, "Thread 1");
        Thread thread2 = new Thread(myTask, "Thread 2");
        thread1.start();
        thread2.start();
    }
}

Because the count member variable is modified without protection from the two threads that have access to it, this yields an unexpected result.
The object variable created inside the run() method is a local variable created by each thread,
and this is why the hash of the object is different.

Output:

[Thread 2] java.lang.Object@33696fea
[Thread 1] java.lang.Object@2d92d0a6
[Thread 2] Count: 46993
[Thread 1] Count: 64307

Different Runnables in multiple threads

package dev.bitek.multithreading.example14;

public class ThreadExample14 {
    // Same MyTask class as previous example

    public static void main(String[] args) {
        Runnable myTask1 = new MyTask();
        Runnable myTask2 = new MyTask();

        Thread thread1 = new Thread(myTask1, "Thread 1");
        Thread thread2 = new Thread(myTask2, "Thread 2");
        thread1.start();
        thread2.start();
    }
}

As each thread is executing its own Runnable instance, there is no longer shared memory between them.

Output:

[Thread 1] java.lang.Object@688e8b6f
[Thread 2] java.lang.Object@38381c73
[Thread 1] Count: 42000
[Thread 2] Count: 42000

Different Runnables with shared object between threads

package dev.bitek.multithreading.example15;

public class ThreadExample15 {
    public static class MyTask implements Runnable {

        // The count member variable is shared between multiple threads
        // that are executing the same instance of the MyTask runnable.
        private int count = 0;

        private Object object;

        // The object member variable can be shared between multiple threads if the same object instance is passed to MyTask executing in multiple threads
        public MyTask() {
        }

        public MyTask(Object object) {
            this.object = object;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();

            // this.object
            System.out.printf("[%s] %s\n", threadName, this.object);

            // i is a local variable created on the thread stack of the current thread
            for (int i = 0; i < 42_000; i++) {
                this.count++;
            }

            System.out.printf("[%s] Count: %d\n", threadName, this.count);
        }
    }
    public static void main(String[] args) {
        Object obj = new Object();

        // obj instance is shared between two different Runnable instances
        // therefore both threads have access to the same object instance
        Runnable myTask1 = new MyTask(obj);
        Runnable myTask2 = new MyTask(obj);

        Thread thread1 = new Thread(myTask1, "Thread 1");
        Thread thread2 = new Thread(myTask2, "Thread 2");
        thread1.start();
        thread2.start();
    }
}

Same object hash in both threads means the Object instance is shared between the two threads.

Output:

[Thread 2] java.lang.Object@77359531
[Thread 1] java.lang.Object@77359531
[Thread 2] Count: 42000
[Thread 1] Count: 42000

Same Runnable instance with shared object between threads

package dev.bitek.multithreading.example16;

public class ThreadExample16 {
    // Same MyTask class as previous example

    public static void main(String[] args) {
        Object obj = new Object();

        // obj instance is shared between the two threads because each thread is using the same Runnable instance
        Runnable myTask = new MyTask(obj);

        Thread thread1 = new Thread(myTask, "Thread 1");
        Thread thread2 = new Thread(myTask, "Thread 2");
        thread1.start();
        thread2.start();
    }
}

Output:

[Thread 1] java.lang.Object@487df085
[Thread 2] java.lang.Object@487df085
[Thread 1] Count: 45057
[Thread 2] Count: 53188

Different Runnables with distinct object between threads

package dev.bitek.multithreading.example17;

public class ThreadExample17 {
    // Same MyTask class as previous example

    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();

        Runnable myTask1 = new MyTask(obj1);
        Runnable myTask2 = new MyTask(obj2);

        Thread thread1 = new Thread(myTask1, "Thread 1");
        Thread thread2 = new Thread(myTask2, "Thread 2");
        thread1.start();
        thread2.start();
    }
}

Output:

[Thread 2] java.lang.Object@7a5b369a
[Thread 1] java.lang.Object@66a71f1b
[Thread 2] Count: 42000
[Thread 1] Count: 42000

Synchronized access to shared data between two threads

A possible solution to ensure integrity of the data shared between the two threads is as follows:

package dev.bitek.multithreading.example18;

public class ThreadExample18 {
    public static class MyTask implements Runnable {
        private int count = 0;

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();

            // Move this line inside the synchronized block of the for loop if you want to see context switching in action
            System.out.printf("[%s] running\n", threadName);

            for (int i = 0; i < 42_000; i++) {
                synchronized (this) {
                    this.count++; // read & write operation
                }
            }

            // read operation
            synchronized (this) {
                System.out.printf("[%s] Count: %d\n", threadName, this.count);
            }
        }

        public synchronized int getCount() {
            return this.count;
        }
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        MyTask myTask = new MyTask();

        Thread thread1 = new Thread(myTask, "Thread 1");
        Thread thread2 = new Thread(myTask, "Thread 2");

        thread1.start();
        thread2.start();

        // Read count shared variable while its modified by two threads
        sleep(8);
        System.out.printf("Count at time t0: %d\n", myTask.getCount());

        sleep(10);
        System.out.printf("Count at time t1: %d\n", myTask.getCount());

        sleep(12);
        System.out.printf("Count at time t2: %d\n", myTask.getCount());
    }
}

Output:

[Thread 1] running
[Thread 2] running
Count at time t0: 5806
Count at time t1: 52667
[Thread 1] Count: 72709
[Thread 2] Count: 84000
Count at time t2: 84000

The count value is 84000 at the end because each thread increments the count variable for 42000 times.
In the output above, thread 1 finished its work when the count variable had the value 72709
and after that thread2 continues to increment the count variable until it reaches its 42000 limit.

Due to the way multitasking is implemented at the OS and CPU level, i.e. via context switching,
for some time thread1 counts and thread2 waits in line for CPU time,
then thread2 is executing the count and thread1 waits and so on.