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:
By Extending the Thread Class
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:
New: A thread is in the "New" state when it is created but not yet started.
Runnable: A thread is in the "Runnable" state when it is ready to run but waiting for the CPU to be assigned to it.
Blocked: A thread is in the "Blocked" state when it is waiting for a resource that is currently being used by another thread.
Waiting: A thread enters the "Waiting" state when it is waiting for another thread to perform a particular action, such as a
join()
method.Timed Waiting: A thread is in the "Timed Waiting" state when it is waiting for a specific amount of time.
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:
- 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.
- 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.
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.