Getting started with Multithreading in Java

Hemant BesraHemant Besra
8 min read

What is multithreading?

Multithreading is a technique in computer programming where multiple threads are used to execute tasks concurrently within a single process. Threads are independent sequences of instructions that can be scheduled and run simultaneously, allowing for parallel execution of different parts of a program. This can improve performance, responsiveness, and resource utilization. Multithreading enables some tasks to be executed in the background while the main program continues to run which helps to do the entire operation with less amount of time.

Advantages of multithreading

  1. Concurrency: Multithreading allows for the concurrent execution of multiple threads within a program, enabling tasks to run simultaneously and make efficient use of available system resources.

  2. Performance improvement: By utilizing multiple threads, time-consuming operations can be executed in parallel, resulting in improved performance and faster execution times for computationally intensive tasks.

  3. Responsiveness: Multithreading helps maintain the responsiveness of a program by separating time-consuming operations from the main thread. This ensures that the user interface remains active and responsive even when performing resource-intensive tasks.

  4. Resource utilization: Multithreading allows for better utilization of modern multi-core processors. By distributing tasks across multiple threads, the processor's cores can be effectively utilized, maximizing overall system performance.

  5. Asynchronous operations: Multithreading facilitates asynchronous programming, enabling tasks to be executed in the background without blocking the main thread. This is particularly useful for handling I/O operations, such as reading from or writing to files or network communication, where waiting for these operations to complete would cause unnecessary delays.

  6. Scalability: Multithreading provides a scalable approach to handle increasing workloads. As the number of threads increases, more tasks can be executed concurrently, allowing for efficient utilization of resources and accommodating higher levels of demand.

  7. Modularity and code organization: Multithreading encourages modular programming by separating different tasks into separate threads. This promotes code organization and can lead to cleaner, more maintainable code.

  8. Real-time applications: Multithreading is essential for developing real-time applications where timely response and quick processing are critical. By utilizing multiple threads, these applications can handle time-sensitive tasks and ensure timely execution.

Let's write a program without multithreading, then with multithreading and see the difference.

Without multithreading

public class Demo {    
    public static void main(String[] args) {
        Long start,end ;
        //we have task1 which takes 3 sec and task2 which takes 2 sec        
        Task1 t1 = new Task1();
        Task2 t2 = new Task2();
        start = System.currentTimeMillis();
        t1.run(); t2.run();
        end  = System.currentTimeMillis();        
        System.out.println(end - start);
    }
}
class Task1{
    public void run(){
        try {
            //3 sec sleep
            Thread.sleep(3000);
        } catch (InterruptedException e) {e.printStackTrace();}
    }
}
class Task2{
    public void run(){
        try {
            //2 sec sleep
            Thread.sleep(2000);
        } catch (InterruptedException e) {e.printStackTrace();}
    }
}

Output -> 5014

The above program took nearly 5 sec to run. Now lets check how much time it will take if we use a thread.

With multithreading

public class Demo {    
    public static void main(String[] args) {
        Long start,end ;
        //we have task1 which takes 3 sec and task2 which takes 2 sec        
        Task1 t1 = new Task1();
        Task2 t2 = new Task2();
        start = System.currentTimeMillis();
        t1.start(); t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {e.printStackTrace();} 
        end  = System.currentTimeMillis();        
        System.out.println(end - start);
    }
}
class Task1 extends Thread{
    public void run(){
        try {
            //3 sec sleep
            Thread.sleep(3000);
        } catch (InterruptedException e) {e.printStackTrace();}
    }
}
class Task2 extends Thread{
    public void run(){
        try {
            //2 sec sleep
            Thread.sleep(2000);
        } catch (InterruptedException e) {e.printStackTrace();}
    }
}

Output -> 3007

You can see above, it took 3 sec to complete both tasks when we used thread whereas without thread same two tasks take 5 sec. Hence thread reduces the execution time by running tasks concurrently.

Now if you are not aware of threads in Java then you must be asking below questions.

  1. Why are Task1 and Task2 extending the Thread class

  2. why did we call the start method rather than the run method?

  3. why we called the join method

I will explain all of this, please continue reading the blog.

How to Create Threads?

There are two ways to create Threads in Java one is by extending the Thread class and the other one is by implementing the Runnable class.

I prefer to use the interface Runnable because if I extend the Thread class i lose the opportunity/scope of extending other required classes.

Extending Thread class

see the below syntax to create a thread by extending Thread class

// the task class should extend Thread class like below
class Task extends Thread{
    public void run(){
        //write your logic/task here
    }
}
  • The above class Task is a thread class as it extends the class Thread

  • Once you extend the Thread class you need to implement the run method where you should write your logic to do the task.

  • Once you created a thread class, the next step is to instantiate the class and start the thread, like below.

public class Demo {    
    public static void main(String[] args) {        
        Task thread = new Task();
        thread.start();
    }
}
  • In the above code, we created an object of the Task class, then we called the start method and this is the way to start a thread to execute its tasks concurrently with other threads including the main thread.

Implementing Runnable interface

See the below syntax to create a thread by implementing Runnable interface

//the task class should implement Runnable interface like below
class Task implements Runnable{
    public void run(){
        //write your logic/task here
    }
}
  • The above class Task is a thread class as it implements the interface Runnable

  • Once you implement the Runnable interface you need to implement the run method where you should write your logic to do the task.

  • Once you created a thread class, the next step is to instantiate the class and start the thread, like below

public class Demo {    
    public static void main(String[] args) {
        Task t = new Task();
        Thread thread = new Thread(t);

        thread.start();
    }
}
  • In the above code, we created an object of the Task class t and then we created an object of the thread class by passing the object of Task class t.

  • Then we called the start method using the object of the Thread class to start the execution of the task as a thread.

Why are we calling the start method when we wrote our logic in the run method?

If we call the run method directly then it will execute the run method as a normal method, it won't act as a thread. we will lose the benefit of threads.

But when we call the start method, the start method of the Thread class takes the object and registers the object as thread and it calls the run method from that thread so that the run method will be executed concurrently with other registered threads.

What is the use of the join() method?

To explain the join method let's take an example.

  • Let's say we have to get data from two tables table1 and table2. once we get those data we need to insert a few data from the table1 and a few data from the table2 into the table3.

  • Now getting data from table1 and table2 is two different tasks and those task we can execute parallelly. But we can not insert data into table3 until we have received data from table1 and table2.

  • That means inserting data into table3 should wait for task 1 and task 2 to complete. In this type of scenario, we should use the join() method.

Problem Code when we don't use join method

public class Demo {    
    public static void main(String[] args) {
        Task t1 = new Task("table1");
        Thread thread1 = new Thread(t1);        
        Task t2 = new Task("table2");
        Thread thread2 = new Thread(t2);

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

        System.out.println("Inserting data into table 3");
        System.out.println("Inserted data into table 3");

    }
}
//the task class should implement Runnable interface like below
class Task implements Runnable{
    private String table;    
    public Task(String table) {
        this.table=table;
    }    
    public void run(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {e.printStackTrace();}
            System.out.println("Got data from table " +table+ " for id : " +i);
        }        
    }
}

Output:

Here we can see that we have not used the join() method that is why mainThread,thread1 and thread2 all 3 are running concurrently and we can see main thread did not wait fro other two thread to complete hence even though it didn't receive the data it started inserting.

Solution Code using join method

package com.hk.basic;

public class Demo {    
    public static void main(String[] args) {
        Task t1 = new Task("table1");
        Thread thread1 = new Thread(t1);        
        Task t2 = new Task("table2");
        Thread thread2 = new Thread(t2);

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

        try {
            thread1.join();//from here main thread will wait for thread1 to complete
            thread2.join();//from here main thread will wait for thread2 to complete
        } catch (InterruptedException e) {e.printStackTrace();}


        System.out.println("Inserting data into table 3");
        System.out.println("Inserted data into table 3");

    }
}
//the task class should implement Runnable interface like below
class Task implements Runnable{
    private String table;    
    public Task(String table) {
        this.table=table;
    }    
    public void run(){
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {e.printStackTrace();}
            System.out.println("Got data from table " +table+ " for id : " +i);
        }        
    }
}

Output:

Here we have called the join() method from both the thread object inside main thread, and the moment join() method is called main thread will wait till those task are done.

NOTE: We have used try catch block cause those codes throw checked exception.

Next Blog :

Multithreading with java 8

0
Subscribe to my newsletter

Read articles from Hemant Besra directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Hemant Besra
Hemant Besra

Experienced Full Stack Java developer. Have Strong Experience in JSP/Servlet, JSF, Jasper Report, Spring Framework, hibernate, Angular 5+, Microservices. Experienced in Front-end technologies such as HTML, CSS, JavaScript, angular 6+, AJAX, JSON, and XML. Strong Hands-on experience on working with Reactive Forms to build form-based application in Angular 6+.