How to PROPERLY handle multithreading in Java? [SOLVED]


Written By - Bashir Alam
Advertisement

Multi-threading is a powerful technique that allows a Java program to execute multiple threads concurrently. This can lead to significant performance improvements in certain types of applications, particularly those that involve a lot of input/output or CPU-bound tasks.

In Java, multi-threading is accomplished using the Thread class, which represents a single thread of execution. Each thread runs independently of other threads and can be scheduled by the JVM to execute on one or more CPU cores. However, multi-threading can be complex and difficult to manage, especially in large-scale applications. Developers must be careful to avoid race conditions, deadlocks, and other common concurrency issues. In this article, we'll explore some of the key techniques and best practices for handling multi-threading in Java.

 

Creating Threads in Java

Creating threads in Java is a powerful technique for executing code concurrently, which can lead to significant performance improvements in certain types of applications. In Java, there are two main ways to create threads: extending the Thread class and implementing the Runnable interface. Let us now discuss both ways of creating threads in java.

 

Method-1: Extending the Thread class

The first way to create a thread is by extending the Thread class. This is done by creating a new class that extends the Thread class and overrides the run() method. The run() method is the entry point for the thread and is where the thread's code is executed.

Here's an example of how to create a new thread by extending the Thread class:

public class MyThread extends Thread {
    public void run() {
        // code to be executed in this thread
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
            try {
                Thread.sleep(1000); // sleep for 1 second
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

In this example, we have created a new class called MyThread that extends the Thread class. The run() method contains the code that will be executed when the thread is started.

In this example, the thread will print out its ID and a counter from 0 to 9, sleeping for 1 second between each iteration. The Thread.sleep() method is used to pause the thread for the specified number of milliseconds. If the thread is interrupted while it is sleeping, an InterruptedException is thrown.

To start the thread, we create an instance of the MyThread class and call its start() method:

Advertisement
MyThread thread = new MyThread();
thread.start();

When the start() method is called, the JVM creates a new thread and calls the run() method. The new thread runs concurrently with the main thread, allowing the program to perform two or more tasks simultaneously.

Here's the full code for this example:

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

class MyThread extends Thread {
    public void run() {
        // code to be executed in this thread
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
            try {
                Thread.sleep(1000); // sleep for 1 second
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Output:

Main thread finished
Thread 12: 0
Thread 12: 1
Thread 12: 2
Thread 12: 3
Thread 12: 4
Thread 12: 5
Thread 12: 6
Thread 12: 7
Thread 12: 8
Thread 12: 9

This output shows that the main thread finishes before the MyThread thread finishes executing its code. This is because the two threads are running concurrently.

 

Method-2: Implementing the Runnable interface

Implementing the Runnable interface is another way to create a thread in Java. In this approach, we create a new class that implements the Runnable interface and implements the run() method, which is the entry point for the thread's code.

Here's an example of how to create a new thread by implementing the Runnable interface:

public class MyRunnable implements Runnable {
    public void run() {
        // code to be executed in this thread
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
            try {
                Thread.sleep(1000); // sleep for 1 second
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

In this example, we have created a new class called MyRunnable that implements the Runnable interface. The run() method contains the code that will be executed when the thread is started.

In this example, the thread will print out its ID and a counter from 0 to 9, sleeping for 1 second between each iteration. The Thread.sleep() method is used to pause the thread for the specified number of milliseconds. If the thread is interrupted while it is sleeping, an InterruptedException is thrown.

Advertisement

To start the thread, we create an instance of the MyRunnable class and pass it to a new Thread object:

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();

When the start() method is called on the Thread object, the JVM creates a new thread and calls the run() method on the MyRunnable object. The new thread runs concurrently with the main thread, allowing the program to perform two or more tasks simultaneously.

Here's the full code for this example:

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        System.out.println("Main thread finished");
    }
}

class MyRunnable implements Runnable {
    public void run() {
        // code to be executed in this thread
        for (int i = 0; i < 10; i++) {
            System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
            try {
                Thread.sleep(1000); // sleep for 1 second
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Output:

Main thread finished
Thread 12: 0
Thread 12: 1
Thread 12: 2
Thread 12: 3
Thread 12: 4
Thread 12: 5
Thread 12: 6
Thread 12: 7
Thread 12: 8
Thread 12: 9

This output shows that the main thread finishes before the MyRunnable thread finishes executing its code. This is because the two threads are running concurrently.

 

Synchronization in Java

Synchronization in Java is the process of controlling access to shared resources by multiple threads. When two or more threads access a shared resource simultaneously, they can interfere with each other, leading to incorrect results or even crashes. Synchronization ensures that only one thread at a time can access a shared resource, preventing interference and ensuring the correct behavior of the program.

Advertisement

In Java, synchronization is achieved through the use of locks. A lock is an object that can be acquired by a thread to gain exclusive access to a shared resource. When a thread acquires a lock, all other threads that try to acquire the same lock are blocked until the first thread releases the lock.

Let's look at an example to see how synchronization works in Java:

public class SynchronizedCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

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

In this example, we have a class called SynchronizedCounter that has a private integer field called count. We have two methods that modify and read the count field: increment() and getCount(). Both methods are marked as synchronized, which means that they can only be accessed by one thread at a time.

Now let's create two threads that access the same instance of the SynchronizedCounter class:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter counter = new SynchronizedCounter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                counter.increment();
            }
        });

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

        System.out.println(counter.getCount());
    }
}

Output:

2000000

In this example, we create two threads, t1 and t2, that both increment the count field of the SynchronizedCounter instance. We start both threads and then wait for them to finish using the join() method. Finally, we print out the value of the count field.

Because the increment() and getCount() methods of the SynchronizedCounter class are synchronized, only one thread can access them at a time. This ensures that the count field is always incremented by exactly one for each increment() call, even when multiple threads are accessing it simultaneously.

Without synchronization, it is possible for multiple threads to access and modify the count field simultaneously, leading to race conditions and incorrect results. Synchronization ensures that the count field is always accessed by only one thread at a time, preventing race conditions and ensuring the correct behavior of the program.

 

Summary

Multi-threading is the concept of executing multiple threads concurrently within a single program. In Java, multi-threading is implemented using the Thread class and the Runnable interface.

To create a new thread in Java, you can either create a subclass of the Thread class and override its run() method, or you can implement the Runnable interface and pass an instance of your Runnable implementation to the Thread constructor.

Advertisement

Once you create a new thread, you can start it by calling its start() method. This will execute the thread's run() method concurrently with the main thread.

To communicate between threads in Java, you can use synchronization mechanisms such as the synchronized keyword, locks, and semaphores. These mechanisms allow you to ensure that only one thread accesses a shared resource at a time and prevent race conditions and other concurrency issues.

Java also provides several high-level abstractions for multi-threading, such as the Executor framework and the java.util.concurrent package. These abstractions provide a more efficient and flexible way of managing threads and tasks.

It's important to note that multi-threading can introduce several issues, such as race conditions, deadlocks, and thread safety problems. To avoid these issues, it's important to carefully design and test your multi-threaded code and use proper synchronization mechanisms and best practices.

 

Further Reading

Threads in Java 

 

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment