Multi Threading Basics in Java

What is a thread?

Anmol Sehgal
11 min readAug 7, 2018

A thread is defined at an operating system level. In the language(like Java), the user leverages the power given by the operating system. In the developer’s point of view a thread is a set of instructions. There can be as many threads in a single application, and different threads can be executed at the “same time”. The JVM itself works with several threads, like Garbage collection, Just-In-Time compiler etc.
Take for example we are writing some text file in say MS word, and at the same time the Spell Check is running in the background which triggers visual signal when we make any typo. And we can launch print command, and it wont stop us from writing or the spell check. All these operations are happening at the same time.

At the CPU level:
Case 1: CPU with single core
So it can only do one thing at a time, so a slice of time will be given to writing a document, then next slice maybe to the spell check, and it goes on. Say on the timeline, it is like:

So only one thing is done at a particular period of time, at the CPU level. So nothing is happening at the same time in Single core CPU.
But we do have the feeling at all these 3 things occur at same time, because they occur so fast(like in the time frame of about 10ms), so we are unable to differentiate these events separately.

Case 2: CPU with multiple cores:
Here 2 cores can do 2 things at same time because now the CPU is able to run 2 things at “same time” as shown:

CPU Sharing:

Considering the above example, there muse be some mechanism by which CPU decides which task to run. There is a Thread Scheduler which is responsible for this CPU sharing.
There are 3 reasons for the scheduler to pause a thread:
1. CPU must be shared equally among different resources.
2. The thread might be waiting for more data.
3. A thread waiting for another thread to do something e.g. waiting for another thread to release a resource.

Race Condition:

It deals with the excess of data concurrently, which means that 2 different threads might be reading/writing same data(same field/variable etc) at the same time, which may raise problems. This is called race condition.

Analysis of Race Condition:

Take for example the infamous Singleton Pattern as below:

1. public class DatabaseManager {2.    private static DatabaseManager INSTANCE = null;

3. private DatabaseManager() {
// Complex code to connect and initialize the database.
4. }

5. public static DatabaseManager getInstance() {
6. if (INSTANCE == null) {
7. INSTANCE = new DatabaseManager();
8. }
9. return INSTANCE;
10. }

Now consider 2 threads calling getInstance() at same time in single core CPU.
Let thread scheduler gave Thread1 the priority, so Thread 2 is waiting.

Thread 1: Check if (INSTANCE == null) at line 6, which is true
Thread 2: Waiting

*The Thread scheduler pauses Thread 1, and gives to Thread 2*

Thread 1: Waiting
Thread 2: Check if (INSTANCE == null) at line 6, which is true for Thread 2 for Thread 2, and it creates the Instance i.e. Line 7 and copies it into Private static field variable.

*The Thread scheduler pauses Thread 2, and gives to Thread 1*

Thread 1(already inside If block): Creates new Instance at Line 7 again and copies new value into static field variable, thus erasing the instance created by Thread 2.
Thread 2: Waiting.

Thus this is a typical example of a race condition.

Synchronization:

To prevent the race condition, we use synchronization in Java. It prevents a block of code to be executed by a single thread at a same time.
So using thee below code we can prevent race condition as:

1. public class DatabaseManager {2.    private static DatabaseManager INSTANCE = null;

3. private DatabaseManager() {
// Complex code to connect and initialize the database.
4. }

5. public static synchronized DatabaseManager getInstance() {
6. if (INSTANCE == null) {
7. INSTANCE = new DatabaseManager();
8. }
9. return INSTANCE;
10. }

So synchronized is like protecting a block of code(here getInstance() method) with a fence, to allow only one component(Thread) to enter it. Or say the Thread which has the lock can enter this fence.
So when a thread wants to enter which synchronized block, it will ask for the key. If the block of code has the key, it will give it this thread, which can then enter and run this method freely. If another wants to enter this, it will make same request again, but now as it has no key available so that thread will wait for the key to get available. Thus it can prevent more than 1 thread to read/write/execute the same block of code.

Lock Object:

For the synchronization to work, we need a special object what will hold the key. In fact every Java object can be made this lock. This is also called Monitor. As in the previous code, we used synchronized on the static method signature which signifies the class(DatabaseManager.java) being used as a lock object.
And if the synchronization is over the non-static method like :

private synchronized methodName() {
// Method body
}

Here the key is the instance of the class it is in.
The third possibility is to use a dedicated object as a lock object as:

private final Object key = new Object();private methodName() {
synchronized(key){
// Method body
}
}

So instead of synchronizing the method, we can create a synchronized block inside this method, and pass this key object as a parameter. This is probably a good idea to hide an object used for synchronization.

Synchronizing More than one method:

Lets say we have 2 methods for Employee class:
getId()
getName()

One way to synchronize them is using synchronized keyword in the method signature. This way we will use the class level lock.
Suppose thread 1 calls getId() method. It will get the key, which is class level(Employee.class) lock. Then even if Thread 2 wants to call another method getName(), it cant because the lock is already with Thread 1, as both uses same lock that is the class level lock.
This is definitely not what we need.

Another approach can be to have 2 Objects, and synchronizing the block of code in these 2 methods on these 2 different objects. This way even if one thread is in a method, another thread can execute another method as both methods use different keys for synchronization.

Now say we have 2 EMployees, say EmpA and EmpB.

EmpA.java
public synchronized String getName()
public synchronized int getId()

EmpB.java
public synchronized String getName()
public synchronized int getId()

Say Thread1 is executing getName() of EmpA. Now it wont stop Thread 2 to execute getName() of EMpB or getId() of EmpB. Its because the methods uses instance locks for synchronization. So EmpA and EmpB uses 2 different objects for synchronization, hence it Thread1 on EmpA will not prevent Thread2 to execute a method from EmpB.

However If we want that if one thread is executing getName() in any instance of Employee(EmpA or EmpB), then no other thread should be able to exexcute getName() of any other Employee. For this we can use class level key lock. Now as key belongs to class, and key being common, no 2 threads can get this key at same time, hence only one thread can run the method at one time in all instances of Employees.
To do this we can have static synchronization on that method.

Reentrant Locks:

Suppose we have 2 instance of Employee class say EmpA and EmpB.
Now say synchronized getName() of EmpA calls synchronized getName() of EmpB. Also let both these getName() methods be synchronized on same key.
Thread which is running synchronized getName() of EmpA can also enter synchronized getName() of EmpB.
Thread1 will get the key to enter synchronized getName() of EmpA. Now when it tries to enter synchronized getName() of EmpB, it asks if that key is available, and since the key is not available, no thread can enter that method. But here the thread asking for the key is precisely the thread having that key. So this is an exception to the rule called Reentrance, and thus the thread can enter synchronized getName() for EmpB as well.
It is very natural to understand.

DeadLocks:

Say the synchronized getName() of EmpA needs KeyA and the synchronized getName() of EmpB needs KeyB.
Now Thread1 kets KeyA and enters getName() of EmpA,
and Thread2 gets KeyB and enters getName() of EmpB.

Now if getName() of EmpA calls getName() of EmpB, then its a deadlock.
It is because as for thread 1 to enter getName() of EmpB, it required keyB which is already held by thread 2.
And for thread 2 to enter getName() of EmpA, it required keyA which is already held by thread 1.
So both will wait for the keys to get released, and it is a deadlock situation as the keys will never be released.

Do deadlock is a situation where Thread T1 holds a key needed by Thread T2, and Thread T2 holds a key needed by Thread T1.

Fortunately JVM can detect the deadlock situations and can logs information to help debug the code.
But unfortunately there is nothing much we can do if the deadlock occurs, besides rebooting the JVM.

Creating Threads in Java

There are 2 ways to create threads in Java:

class ThreadDemo extends Thread {
@Override
public void run() {
// Some code here...
}
}

class RunnableDemo implements Runnable {
@Override
public void run() {
// Some code here...
}
}

public class MainClass {

public static void main(String[] args) {
// Way 1
new ThreadDemo().start();

//Way 2
new Thread(new RunnableDemo()).start();
}
}

As shown, we can either create thread class by extending Thread.java which limits our class to extend any different class as java supports only Single Inheritance. And then there is second way by which we can pass Runnable instance while creating thread, and then starting it as usual. This is better approach as now instead of extending the thread, we are implementing an Interface.

Race Condition Example:

Suppose we have a number utility class, which performs some addition. And let this increments by one(for simplicity). Now if the initial number is 0, and this method is called say 1000 times by 1000 different threads, we expect the output to be 1000*1000 = 1,000,000 right?
Lets see what happens:

/**
* Runnable class which increments 1 to the number in its own thread.
*/
class NumIncrementRunnable implements Runnable {
private NumberUtils numberUtils;

public NumIncrementRunnable(NumberUtils numberUtils) {
this.numberUtils = numberUtils;
}

@Override
public void run() {
for (int x = 0; x < 1000; x++) {
// Increment a number 1000 times.
numberUtils.increment();
}
}
}

/**
* Utility class having the increment method.
*/
class NumberUtils {
private int num;

public NumberUtils(int num) {
this.num = num;
}

public int getNum() {
return num;
}

public void increment() {
num = num + 1;
}
}

public class MainClass {

public static void main(String[] args) throws InterruptedException {

// Initial number = 0
NumberUtils numberUtils = new NumberUtils(0);

// Create 100 threads, each incrementing 1000 times
Thread[] threads = new Thread[1000];

for (int x = 0; x < 1000; x++) {
threads[x] = new Thread(new NumIncrementRunnable(numberUtils));
threads[x].start();
}

// Waiting for all the threads to complete their execution.
for (int x = 0; x < 1000; x++) {
threads[x].join();
}

// Print the final output.
// Should be 1000*1000 = 1,000,000
System.out.print(numberUtils.getNum());
}
}

We are waiting for a ll the threads to complete their execution, by join() on all of them. We expect the number to be 1,000,000 always, but we get the output always different, like it can be 987564 or 967879 or anything.
So this is the classical example of Race Condition. It is because of the increment method

public void increment() {
num = num + 1;
}

This single statement has 3 steps:
1. Reads the value of num
2. Increments the value of the num by one
3. Stores the new value to the num i.e. write operation
So this single statement has read and write operation from different threads at the same time, hence the race condition and the wrong output.

To fix the code, use synchronize as:

class NumberUtils {
private int num;
private Object key = new Object();

public NumberUtils(int num) {
this.num = num;
}

public int getNum() {
return num;
}

public void increment() {
synchronized (key) {
num = num + 1;
}
}
}

And now it will always be 1,000,000. Its because only one thread tries to read and write to the number.

Deadlock Example:

Let we have 2 keys for locking, used by 3 methods.
MethodOne() takes Key1 and tries to call methodTwo() which requires Key2.
MethodTwo() takes Key2 and tries to call methodThree() which requires Key1.
MethodThree() takes Key1.
This is a possible deadlock situation as if someone calls methodOne and methodTwo at same time, as then key1 will be held by methodOne and waiting to get key2 to execute methodTwo, which is held by methidTwo, which inturn is waiting to get key1 to execute methodThree.

class DeadlockDemo {
private Object key1 = new Object();
private Object key2 = new Object();

public void methodOne() {
synchronized (key1) {
System.out.println("In methodOne " + Thread.currentThread().getName());
methodTwo();
}
}

private void methodTwo() {
synchronized (key2) {
System.out.println("In methodTwo " + Thread.currentThread().getName());
methodThree();
}
}

private void methodThree() {
synchronized (key1) {
System.out.println("In methodThree " + Thread.currentThread().getName());
}
}
}

Running this like:

public class Main {
public static void main(String[] args) throws InterruptedException {
final DeadlockDemo deadlockDemo = new DeadlockDemo();

Runnable r1 = new Runnable() {
@Override
public void run() {
deadlockDemo.methodOne();
}
};

Runnable r2 = new Runnable() {
@Override
public void run() {
deadlockDemo.methodTwo();
}
};

Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);

t1.start();
t2.start();

t1.join();
t2.join();
}
}

Output:
In methodOne Thread-0
In methodTwo Thread-1

So first thread is blocked and cant enter methodTwo and
So second thread is blocked and cant enter methodThree.

Java Thread States

The threads in java can be in various different states.
These states can be:

NEW

When we create a thread using Thread t = new Thread()
It has not run or executed its task yet.

RUNNABLE

Once the thread.start() is called, it is in runnable state.
Now the Thread Scheduler can give the time slice of the CPU to this thread, so that this thread can execute its task.

TERMINATED

After the runnable has done its task, it is in Terminated state. The thread scheduler knows this thread should not be run anymore.

BLOCKED

When a thread is blocked at the entrance of the Synchronized block because the key of the lock object is not available, it is in blocked state. The Thread scheduler will not try to awake this thread as long as the key is not available. It is a very common state for the thread.

WAITING

Once the wait() method is called it is in the wait state. It can be awaken by the notify call.

TIMED_WAITING

The wait() method can also take a timeout using wait(timeout), and at the end of this timeout the thread will be automatically notified by the system without the need of calling notify on the same key/lock.

The states can be briefly summarized using the below diagram:

--

--

No responses yet