Skip to main content

Java Concurrency 1

·2006 words·10 mins
Table of Contents
Java - This article is part of a series.
Part : This Article

One of the most important features of Java is multi threading. So, before diving deep into this let’s make a few things clear.

What is multitasking?
#

Multitasking is running several activities to occur concurrently. In software, we have 2 types of multitasking,

  • Process based multitasking
  • Thread based multitasking

Process based multitasking
#

This is running multiple programs parallely. For instance, a browser maybe playing music in the background, while we are coding in our IDE.

Thread based multitasking
#

This is running several parts of the program concurrently. For instance, a Word document is an ideal example for this. It formats the code, does spell check, displays the content as its being typed.

Now with this clear, we will see the difference between threads and processes.

Threads v/s Processes
#

Some of the difference between these are

  • Threads are light weight compared to processes.
  • Two threads share the same address space.
  • So, context switching between threads is less expensive than processes.
  • The cost of communication between threads is lower as well.

Why do we go to multithreading?
#

  • In single threaded program, only one task runs at a time.
  • CPU cycles get wasted, while waiting for blocking operations such as I/O.
  • Multi tasking uses the idle CPU time well.

But what is a thread?
#

  • A thread is an independent sequential path of execution within a program.
  • A program can have many threads running concurrently
  • Always a main thread is executed by default.
  • At runtime, threads of a program exist in a common memory space.
  • They also share the process running the thread.

How to create a thread?
#

Before we go on to create a thread, we will learn how a normal java program runs using thread.

Main thread
#

By default when a java application is started, a user thread called main is created to executed the main thread. If no other user threads are spawned, the program terminates after main() method is done executing. All child theads are spawned from the main thread. Even if the main thread completes execution, the program will run till all the user threads are completed.

User vs Daemon threads
#

The runtime diffrencates between main and daemon threads. Calling setDaemon(boolean) marks the thread as a daemon thread or user thread. This has to be done before starting the thread. As long as an user thread is alive, JVM will run. But for a daemon thread this is not true, it’s at the mercy of the runtime. It’s stopped if there are no more runtime threads running, thus terminating the program.

But why do we need these daemon threads. These are low priority threads that can be used to run supporting tasks. It’s not recommended to use these for I/O operations. We can use these threads for garbage collection, removing unwanted objects from cache, etc. For instance, the Garbage collector is a daemon thread running in JVM.

Thread creation
#

A thread in java is represented as an object of Thread class. We can create a thread in 2 ways,

  • Extending the java.lang.Thread class.
  • Implementing the java.lang.Runnable interface.

Let’s try creating a thread by extending the Thread class. We just need to override the run method to implement our logic. We will create a constuctor to accept the thread name.

public class Thread1 extends Thread{
    //Constructor to give a name to the thread
    public Thread1(String threadName){
        super(threadName);
    }
    //Overriding the run method to perform our operation
    @Override
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread 1 "  + Thread.currentThread() + " " + i);
        }
    }
}

To create the thread, we create an instance of the thread and call the start method. We should call the start method to spawn a seperate thread. If run is called, it will get executed as a normal method call in the main thread.

public class Main {
    public static void main(String[] args) {
        System.out.println("Main thread starts");
        Thread1 thread1 = new Thread1("thread1");
        thread1.start();
        System.out.println("Main thread ends");
    }
}

The output is as follow, even after the main thread ends, the spawned thread thread1 continues execution. The currentThread has returned the current thread’s name, priority and the parent thread.

Main thread starts
Main thread ends
Thread 1 Thread[thread1,5,main] 0
Thread 1 Thread[thread1,5,main] 1
Thread 1 Thread[thread1,5,main] 2
Thread 1 Thread[thread1,5,main] 3
Thread 1 Thread[thread1,5,main] 4

Now, if we use the Runnable interface, we have to just implement the run method instead of overriding as with Thread class.

public class Thread2 implements Runnable{
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread 2 "  + Thread.currentThread() + " " + i);
        }
    }
}

But, there is an difference in how we execute the Runnable interface thread. We will look at one way here, and the other ways in the next article.

public class Main {
    public static void main(String[] args) {
        System.out.println("Main thread starts");
        Thread thread2 = new Thread(new Thread2(), "thread2");
        thread2.start();
        System.out.println("Main thread ends");
    }
}

We use the Thread class, pass the instance of a Runnable implementation and a name(optional) to create the thread object. We can’t create an instance of the Thread2 class and call Runnable as there is no start present in the Runnable interface.

Why are these the 2 ways of creating a Thread? Let’s take a look at the run method in the Thread class.

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

It looks if it has an member variable target is present, if present it calls the run of target, else it does nothing. So, while using Runnable, we just provide an implementation for this run method, pass an object of Runnable Implementation to it. The other way, was to directly extend the Thread class and override the method we see above.

We prefer implementing Runnable over extending Thread as we can still keep the class open for extending other classes.

Currently, the trend is to use a lambda function and pass the run method implementation as a paramter to the Thread class.

public class Main {
    public static void main(String[] args) {
        System.out.println("Main thread starts");
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread() + " " + i);
            }
        }, "threadlambda");
        thread.start();
        System.out.println("Main thread ends");
    }
}

This is quite straight forward and we can implement the thread quickly.

Synchronization
#

Now, let’s imagine a scenario where we have 2 threads running for a ticket booking application. Suppose, there are 2 tickets left and 2 seperate users try to access them at the same time, then we run into an issue where both users will be given the tickets. Here arises the need to synchronize the thread on shared resources.

Let’s see this with an example below for the same ticket booking application.

public class TicketBooking {
    private int tickets;
    public TicketBooking(int tickets){
        this.tickets = tickets;
    }

    public void bookTickets(String user, int ticketsToBook){
        if (this.tickets >= ticketsToBook){
            System.out.println(ticketsToBook + " tickets books for " + user);
            this.tickets -= ticketsToBook;
        }
        else {
            System.out.println(ticketsToBook + " tickets not available for " + user);
        }
    }
}
public static void main(String[] args) {
    TicketBooking ticketBooking = new TicketBooking(3);
    Thread ticketBookingThread1 = new Thread(() -> {
        ticketBooking.bookTickets("User 1", 2);
    });
    Thread ticketBookingThread2 = new Thread(() -> {
        ticketBooking.bookTickets("User 2", 2);
    });
    ticketBookingThread1.start();
    ticketBookingThread2.start();
}

What would the output be? Since, we are checking everytime before actually booking the tickets, whether the tickets do exist, we would expect the output to be something like this

2 tickets books for User 1
2 tickets not available for User 2

But the actual execution results in below.

2 tickets books for User 2
2 tickets books for User 1

This is because the 2 threads access the ticket at the same time. To avoid this we need to use synchronization. We achieve synchronization by using lock objects. Any object can be used as a lock.

Let’s try with a simple String, a dummy object, then make the method as synchronized.

Synchronized blocks
#

Using a simple string as a lock.

public void bookTickets(String user, int ticketsToBook){
    synchronized ("lock"){
        if (this.tickets >= ticketsToBook){
            System.out.println(ticketsToBook + " tickets books for " + user);
            this.tickets -= ticketsToBook;
        }
        else {
            System.out.println(ticketsToBook + " tickets not available for " + user);
        }
    }
}

Using a lock object

public class TicketBooking {
    private int tickets;
    Object lock;// lock object
    public TicketBooking(int tickets){
        this.tickets = tickets;
        lock = new Object();//initalizing the lock object, will throw error if its not initialized
    }

    public void bookTickets(String user, int ticketsToBook){
        synchronized (lock){//using the lock object
            if (this.tickets >= ticketsToBook){
                System.out.println(ticketsToBook + " tickets books for " + user);
                this.tickets -= ticketsToBook;
            }
            else {
                System.out.println(ticketsToBook + " tickets not available for " + user);
            }
        }
    }
}

Few points to keep in mind about the synchronized blocks are,

  1. The difference between synchronized methods and blocks is rather than the lock being on the object instance, lock can be some arbitary object.
  2. If object reference expression in the synchronized block evaluates to null, it will throw NullPointerException.

Synchronized methods
#

Using a synchronized method. This is advised if the lock object would be this, i.e the lock object is the instance of the class.

public synchronized void bookTickets(String user, int ticketsToBook){
    if (this.tickets >= ticketsToBook){
        System.out.println(ticketsToBook + " tickets books for " + user);
        this.tickets -= ticketsToBook;
    }
    else {
        System.out.println(ticketsToBook + " tickets not available for " + user);
    }

}

One thing we need to keep in mind is that this can be easily extended to multiple methods. A single lock can be used for different methods. For instance, in the above example, if an admin was to add more tickets, that method would need to use the same lock as the method to book tickets. We can have multiple locks as well in the class.

  1. While thread is executing inside a synchronized method of an object, all other threads which need to execute synchronized methods(either the same method or any other method of the object) have to wait.
  2. This restriction does not apply if the thread that already has the lock is trying to execute other synchronized methods i.e synchronized methods calling other synchronized methods.
  3. Non synchronized methods can be called and executed by other threads at any time.

Static Synchronized methods
#

Synchronization of static methods. Below is an example of a singleton class that is thread safe. We use the class as the lock.

public class Hotel {
    private static volatile Hotel hotelInstance = null;
    private Hotel(){
        System.out.println("Created Hotel");
    }
    public static Hotel getHotelInstance() {
        if (hotelInstance == null){
            synchronized (Hotel.class){
                if(hotelInstance == null){
                    hotelInstance = new Hotel();
                }
            }
        }
        return hotelInstance;
    }
}

Some points to keep in mind for static synchronized methods

  1. A thread acquiring the lock of the class has no effect on any thread trying to acquire the object lock. i.e synchronzied static methods and static instance methods have no relations.
  2. A sub class can descide whether the inherited method stays synchronized or not.

Rules of synchronization
#

  1. A thread must obtain object lock associated with the shared resources before it can enter it.
  2. The JRE ensures that no other thread can enter the shared resources if another thread holds the object lock for the resource.
  3. If a thread cannot get the object lock, it goes into blocked state.
  4. When thread exits shared resource, the JRE ensures the lock is released.
  5. Assumptions about the order in which lock is acquired should not be made.

Race condition
#

It occurs when 2 or more threads simultaneously update the same value and leaves the value as undefined or inconsitent.

Volatile keyword
#

We used the volatile keyword before. Let’s just look about it in brief. Simply put, it means that the variables prefixed with volatile are not read from cache, but direclty from the RAM.

We will look at transistion between thread states, thread scheduler, priorities, in the next article.

Java - This article is part of a series.
Part : This Article