Multithreading and Concurrency in Java

Multithreading and concurrency are two fundamental concepts in Java that enable efficient execution of multiple tasks simultaneously, improving the performance and responsiveness of applications. Let's explore both of these concepts in detail and their implementation in Java.


What is Multithreading?

Multithreading is the ability of a CPU (Central Processing Unit) to execute multiple threads concurrently within a single process. A thread is a lightweight subprocess, and multithreading allows a program to accomplish multiple tasks at the same time, improving efficiency and resource utilization.

Each thread has its own program counter, stack, and local variables, but shares memory and other resources with other threads in the same process. Java provides built-in support for creating and managing threads.


What is Concurrency?

Concurrency refers to the ability of a system to handle multiple tasks or processes at once, whether by running them simultaneously (in the case of multiple processors or cores) or by interleaving tasks within a single processor. In Java, concurrency is primarily achieved through multithreading. It allows programs to perform tasks like I/O operations, calculations, or responding to user actions without blocking other tasks.

Concurrency helps in building scalable and efficient systems, especially in applications where tasks can be performed independently of each other, such as web servers, databases, and real-time applications.


Multithreading in Java: Key Concepts and Implementation

In Java, multithreading can be achieved using two main approaches:

  1. By Extending the Thread Class

  2. By Implementing the Runnable Interface

1. By Extending the Thread Class

Java provides a Thread class that you can extend to create a new thread. This class defines the run() method, which contains the code to be executed by the thread.

Example:

class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread is running: " + i);
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Start the thread

        // Main thread execution
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread is running: " + i);
        }
    }
}

In this example, MyThread extends the Thread class and overrides the run() method to define the code that will run in the new thread. The start() method is used to begin the execution of the thread.

2. By Implementing the Runnable Interface

Another way to create a thread in Java is by implementing the Runnable interface. This approach is more flexible because a class can implement multiple interfaces, unlike extending the Thread class.

Example:

class MyRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnable thread is running: " + i);
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread t1 = new Thread(runnable);
        t1.start(); // Start the thread

        // Main thread execution
        for (int i = 0; i < 5; i++) {
            System.out.println("Main thread is running: " + i);
        }
    }
}

In this approach, we define a run() method inside the Runnable interface, and then pass the Runnable object to the Thread constructor.


Thread Lifecycle

Java threads follow a specific lifecycle, which includes several states:

  1. New: A thread is in the "New" state when it is created but not yet started.

  2. Runnable: A thread is in the "Runnable" state when it is ready to run but waiting for the CPU to be assigned to it.

  3. Blocked: A thread is in the "Blocked" state when it is waiting for a resource that is currently being used by another thread.

  4. Waiting: A thread enters the "Waiting" state when it is waiting for another thread to perform a particular action, such as a join() method.

  5. Timed Waiting: A thread is in the "Timed Waiting" state when it is waiting for a specific amount of time.

  6. Terminated: A thread is in the "Terminated" state when it has finished its execution.


Thread Synchronization

In multithreading, multiple threads can access shared resources simultaneously, which might lead to inconsistent or corrupt data. To avoid this, synchronization is used to ensure that only one thread can access a shared resource at a time.

Synchronization in Java

Java provides two main ways to synchronize code:

  1. Synchronized Methods: You can declare a method as synchronized to ensure that only one thread can execute that method at a time.

Example:

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

public class SyncExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        // Create multiple threads to increment the counter
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

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

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + counter.getCount());
    }
}

In this example, the increment() method is synchronized, ensuring that only one thread can access the method at a time.

  1. Synchronized Blocks: You can synchronize a specific block of code inside a method to minimize the scope of synchronization.

Example:

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

Concurrency Utilities in Java

Java provides a rich set of concurrency utilities in the java.util.concurrent package. These utilities simplify complex concurrency tasks like managing thread pools, semaphores, and managing the synchronization of resources.

Executor Framework

The Executor Framework provides a higher-level replacement for managing threads. It decouples task submission from the mechanics of how each task will be executed, including the details of thread use, scheduling, and execution.

Example:

ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
    System.out.println("Task is running");
});
executor.shutdown();

Locks

Java provides explicit Locks in addition to synchronized blocks. A ReentrantLock allows more control over thread synchronization, such as the ability to attempt to acquire the lock or to interrupt a thread while waiting for the lock.

Example:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // Critical section
} finally {
    lock.unlock();
}

Atomic Variables

Atomic Variables are variables that support atomic operations, meaning the operations are thread-safe without needing synchronization.

Example:

AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet(); // Atomically increments the counter

Conclusion

Multithreading and concurrency in Java enable efficient and parallel execution of tasks, improving the performance of applications, particularly those that need to handle multiple tasks simultaneously. Java provides various mechanisms such as threads, synchronization, and concurrency utilities to manage multiple threads and ensure thread safety. Understanding and implementing these concepts are essential for building high-performance, scalable, and responsive applications.

0
Subscribe to my newsletter

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

Written by

Mohammed Shakeel
Mohammed Shakeel

I'm Mohammed Shakeel, an aspiring Android developer and software engineer with a keen interest in web development. I am passionate about creating innovative mobile applications and web solutions that are both functional and aesthetically pleasing.