A thread is the smallest unit of execution in a program. In Java, a thread can be created by extending the Thread
class or implementing the Runnable
interface.
// Extending the Thread class
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running thread: " + Thread.currentThread().getName());
}
}
// Implementing the Runnable interface
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Running runnable: " + Thread.currentThread().getName());
}
}
public class ThreadExample {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
Thread runnableThread = new Thread(new MyRunnable());
runnableThread.start();
}
}
Synchronization is a mechanism in Java that is used to control access to shared resources by multiple threads. It ensures that only one thread can access a shared resource at a time.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new 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();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
A race condition occurs when multiple threads access and manipulate shared data concurrently, and the final outcome depends on the relative timing of their execution.
class RaceConditionExample {
private static int sharedVariable = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedVariable++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedVariable++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final value of sharedVariable: " + sharedVariable);
}
}
In this example, the final value of sharedVariable
may not be 2000 due to race conditions.
A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.
class DeadlockExample {
private static final Object resource1 = new Object();
private static final Object resource2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Holding resource 1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for resource 2...");
synchronized (resource2) {
System.out.println("Thread 1: Holding resource 1 and 2...");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Holding resource 2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for resource 1...");
synchronized (resource1) {
System.out.println("Thread 2: Holding resource 1 and 2...");
}
}
});
t1.start();
t2.start();
}
}
In this example, both threads may end up waiting for each other indefinitely, resulting in a deadlock.
Resource starvation occurs when a thread is unable to gain regular access to shared resources and is unable to make progress.
As shown in the synchronization example above, using the synchronized
keyword can help prevent race conditions. You can synchronize methods or blocks of code to ensure that only one thread can access the shared resource at a time.
Nested synchronization can increase the risk of deadlocks. Try to keep synchronization simple and avoid locking multiple resources in different orders in different threads.
Java provides many thread-safe data structures such as ConcurrentHashMap
, CopyOnWriteArrayList
, etc. These data structures are designed to be used in a multithreaded environment without the need for external synchronization.
import java.util.concurrent.ConcurrentHashMap;
public class ThreadSafeDataStructureExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
}
}
The Executor framework in Java provides a high - level interface for managing threads. It allows you to create thread pools and submit tasks to them.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorFrameworkExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
System.out.println("Task 1 is running on thread: " + Thread.currentThread().getName());
});
executor.submit(() -> {
System.out.println("Task 2 is running on thread: " + Thread.currentThread().getName());
});
executor.shutdown();
}
}
volatile
KeywordThe volatile
keyword in Java is used to mark a variable as “being stored in main memory”. Any read or write operation on a volatile
variable will be directly from/to the main memory, which can help prevent visibility issues.
class VolatileExample {
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (!flag) {
// Do some work
}
System.out.println("Thread 1: Flag is now true.");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Thread 2: Set flag to true.");
});
t1.start();
t2.start();
}
}
Java multithreading can be a powerful tool for improving the performance and responsiveness of applications, but it comes with its own set of challenges. By understanding the fundamental concepts, being aware of common pitfalls such as race conditions, deadlocks, and resource starvation, and following best practices like using synchronization properly, thread-safe data structures, the executor framework, and the volatile
keyword, developers can write robust and reliable multithreaded Java applications.