Java — Multithreading A tale of Booking an Imaginary(I) Seat

Mahadev K
7 min readJan 15, 2023

Let’s start with some basic intro about multithreading, shall we? Multithreading hmm… 🤔 Well, we can say multitasking we know what that really is… We do that quite often as well. Remember the last time you were on phone while driving your car or listening to a song while coding. It’s quite common but yeah be a little cautious while driving and talking on phone even if you have a Tesla. So, how to do this in code. How do our binary systems handle this idea. Scroll down 🔍.

Threads

Every process executes on a thread that’s what we call them.
There will be a main thread and couple of sub-threads.
The main thread will create the initial sub-threads to execute a logic parallelly.

Creating a thread on java can be done in 4 ways.

  • Make the class extend the thread class implement the run method.
  • Make the class implement Runnable interface and implement the run method.
  • Create thread via anonymous inner class while defining the Thread instance itself.
  • Use Runnable functional interface to create an inline lambda for the process you wanna execute.

Run your thread with

new Thread(<instance of your class>).start().

To execute we require to call start method not run. A call to run method will execute the program on the main thread itself.

A great story of booking an imaginary( i ) seat.

  • Create a class that performs the task.
  • Make this class extend the Thread class.
  • Override the function run present in the Thread class.
  • Call your perform task function inside the run method.
  • Instantiate your class with Type as Thread class.
  • Call the start method to run your program on a sub thread.

The below example shows two threads trying to book a seat for the incoming request. What are the chances?? 🤔.

 
private static void workOnThread(){

BookingService bookingService = new BookingService();
Thread threadA = new ThreadA(bookingService);
Thread threadB = new ThreadB(bookingService);

threadA.start();
threadB.start();

}

public class BookingService {

int totalSeats = 1;

public boolean book() throws InterruptedException {
if(totalSeats>0){
Thread.sleep(10); // To simulate delay
totalSeats— ;
return true;
}else{
return false;
}
}
}
public class ThreadA extends Thread{ 
BookingService bookingService;

public ThreadA(BookingService bookingService){
this.bookingService = bookingService;
}

@Override
public void run(){
boolean booked = false;
try {
booked = bookingService.book();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currentThread().getName()+ “ booked : “+ booked);
}
}
public class ThreadB extends Thread{ 
BookingService bookingService;

public ThreadB(BookingService bookingService){
this.bookingService = bookingService;
}

@Override
public void run(){
boolean booked = false;
try {
booked = bookingService.book();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(currentThread().getName()+ “ booked : “+ booked);
}
}

Considering there is only one seat available, and we have a block of code executing the booking of seat and now two threads try to book the same seat for two different persons. And as you expected both of the person will be happy because they got their tickets for the same seat.

Why this happens?? Consider the first thread starts executing the “if” block and yet not decremented the seats count same time the second thread also starts and starts executing the code, now both transactions will occur causing the two requests getting fulfilled.

Another way will be like the first thread would have updated the total seats but when the thread 2 reads it didn’t get the updated value. For this we have to see the Java Memory Model.

Java Memory Model

Java memory model consists of two main parts -

  • Thread Stack
  • Heap

Each thread will create its own thread stack which will not be visible for the other threads. The thread stack will contain the “call stack” which refers to the methods the thread has called to reach the current point of execution and Local variables initialized by the thread.

Heap is a shared memory which contains all the objects created by java application. Each thread can read the object from heap and work on it.

So where does the problem exist?? 🤔. The answer to that is Hardware Memory Architecture.

Hardware Memory Architecture.

The hardware memory architecture consists of,

  • RAM — Main memory
  • CPU Cache Memory
  • CPU Registers.

RAM contains the heap and the thread stack. Whenever a thread requires to read some values in object stored in heap it reads from heap of RAM and caches those detail in CPU Registers or CPU Cache memory. Now another thread running on another CPU/Core will not be getting updated value when it reads the data from RAM since the previous thread didn’t flush the details to the RAM yet. This causes race conditions.

How to solve?

The problem was that there was a single seat to book which translates to single resource. And the single resource is meant for one person only. So, we need to restrict the threads that can access this resource to a count 1. How to achieve that, well we can use synchronized blocks or some sort of locks which will allow only one thread to execute the critical code section and the rest of the thread can sleep for the time being. Once the current thread is finished with the process it will notify the rest of the threads which are on waiting list in a way sleeping to try and acquire for the lock.

package multithreading; 

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

public class BookingService {

int totalSeats = 1;

public synchronized boolean book() throws InterruptedException {
return bookWithLock();
}

public synchronized boolean bookWithSynchronizedKeyword() throws InterruptedException {
if(totalSeats>0){
Thread.sleep(10); // To simulate delay
totalSeats— ;
return true;
}else{
return false;
}
}

public boolean bookWithSyncBlock() throws InterruptedException {
synchronized (this) {
if (totalSeats > 0) {
Thread.sleep(10); // To simulate delay
totalSeats-- ;
return true;
} else {
return false;
}
}
}

public boolean bookWithLock() throws InterruptedException{
boolean isBooked = false;
Lock lock = new ReentrantLock();
lock.lock();
try{
if (totalSeats > 0) {
Thread.sleep(10); // To simulate delay.
totalSeats-- ;
isBooked = true;
} else {
isBooked = false;
}
}finally {
lock.unlock();
}

return isBooked;
}
}

Thus, we averted the argument that could have occurred for a single seat tough day 😅.

Synchronized and Locks

Both serve the same purpose then what’s the benefit. The answer is flexibility offered by locks. You can lock anywhere in one method and unlock the lock in the same method or any other sub method. So, locks are way more flexible than synchronized key word.

Few other cool things about multithreading

  • Volatile keyword

A volatile keyword helps in maintaining the data in the main memory not in cache. Usually, these data will end up in CPU cache but with volatile it will get reflected in RAM. So other threads when fetching data from RAM will get the updated value.

  • Semaphores

Semaphores are signaling mechanism. This allows specified number of threads to access a shared resource. Like Gas stove is a resource and have multiple burners so you can cook rice and lentils at a time, but the other things will wait() until one of the current process finishes. All the thread tries to acquire the lock once notified. Fairness is another thing — If this is true then the next thread in the queue will get the resource else any thread may acquire the lock.

  • Blocking queue

Blocking queue is just a queue with a limit. This will enqueue item to the queue. If the queue is full the new item will have to wait until there is a space. Dequeue will remove item from queue if present else the thread will wait until an object available for removal.

  • With Respect to database Transaction on Distributed Systems.

So, what if our application is deployed in multiple instances.
Will these locks help when we want to lock the row from being updated or so. On this case there are few things we could do in an application,
we could have repeatable read isolation level for transaction.
Also, have an optimistic locking mechanism using version for each row.
So, when the row is updated, version increases. If there is a version mismatch, then the whole transaction fails.

Overview of what just happened.

To conclude, what we saw

  • What is multithreading?
  • How can we create threads in Java?
  • Java memory model
  • Hardware memory model
  • Race Conditions
  • Synchronized keyword and Locks.
  • Couple of multithreading topics

Github Repo link

mahadev-k/JavaSeries: A Java exploration repository 🚀🚀🚀. (github.com)

Code is pushed in this repo for further references.

References

I would like to thank this awesome creator's articles for inspiring me to write this article.

Java Concurrency by Jakob Jenkov

Connect with me

Another day Another time!!

I think this makes it for the first article on multithreading. Put down your thoughts and improvements that can be made on comment section. Will see you all another day with another interesting post, until then goodbye!

--

--

Mahadev K

Software Developer. Interested in learning and implementing new things. Knows C++, Java, Angular, React. Arduino, Raspberry pi, ESP’s were college mates.