A thread is a lightweight subprocess within a process. In Java, a thread is an instance of the Thread
class or a subclass of it. Each thread has its own call stack, program counter, and local variables. Multiple threads can run concurrently within a single Java program, sharing the same memory space.
// Example of creating a simple thread
class MyThread extends Thread {
@Override
public void run() {
System.out.println("This is a thread running.");
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
A thread in Java can be in one of the following states:
When multiple threads access shared resources concurrently, it can lead to data inconsistency and race conditions. Thread synchronization is used to ensure that only one thread can access a shared resource at a time. In Java, you can use the synchronized
keyword to achieve thread synchronization.
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());
}
}
There are two main ways to create a thread in Java:
Thread
class: You can create a subclass of the Thread
class and override the run()
method.class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running.");
}
}
Runnable
interface: You can implement the Runnable
interface and pass an instance of the class to the Thread
constructor.class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running.");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
To start a thread, you call the start()
method on the Thread
object. Once a thread is started, the run()
method is executed in a separate thread of execution.
Thread thread = new Thread(() -> {
System.out.println("Thread is running.");
});
thread.start();
In Java, there is no direct way to stop a thread. You can use a flag to signal the thread to stop its execution.
class StoppableThread extends Thread {
private volatile boolean stopped = false;
@Override
public void run() {
while (!stopped) {
System.out.println("Thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Thread has stopped.");
}
public void stopThread() {
stopped = true;
}
}
public class StopThreadExample {
public static void main(String[] args) throws InterruptedException {
StoppableThread thread = new StoppableThread();
thread.start();
Thread.sleep(5000);
thread.stopThread();
}
}
The join()
method is used to wait for a thread to complete its execution. This is useful when you want to ensure that one thread has finished before another thread continues.
Thread t1 = new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1 has finished.");
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2 is starting after Thread 1 has finished.");
});
t1.start();
t2.start();
The producer-consumer pattern is a classic multi-threading pattern where one or more producer threads produce data and one or more consumer threads consume the data. A shared buffer is used to store the data.
import java.util.LinkedList;
import java.util.Queue;
class Buffer {
private Queue<Integer> queue = new LinkedList<>();
private static final int MAX_SIZE = 5;
public synchronized void produce(int item) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.add(item);
System.out.println("Produced: " + item);
notifyAll();
}
public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int item = queue.poll();
System.out.println("Consumed: " + item);
notifyAll();
return item;
}
}
class Producer implements Runnable {
private Buffer buffer;
public Producer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
buffer.produce(i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
private Buffer buffer;
public Consumer(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
buffer.consume();
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread producerThread = new Thread(new Producer(buffer));
Thread consumerThread = new Thread(new Consumer(buffer));
producerThread.start();
consumerThread.start();
}
}
Thread pooling is a technique where a fixed number of threads are created in advance and reused to execute multiple tasks. This can reduce the overhead of creating and destroying threads. In Java, you can use the ExecutorService
interface to implement thread pooling.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is being executed by " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
}
}
A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource. To avoid deadlocks, you can follow these guidelines:
Atomic variables in Java provide a way to perform atomic operations without using locks. This can improve the performance of your multi-threaded applications.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicVariableExample {
private static AtomicInteger atomicCount = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCount.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
atomicCount.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final atomic count: " + atomicCount.get());
}
}
In multi-threaded applications, exceptions can occur in any thread. It is important to handle exceptions properly to avoid unexpected behavior. You can use a try-catch
block in the run()
method of your threads.
Thread thread = new Thread(() -> {
try {
// Code that may throw an exception
int result = 1 / 0;
} catch (ArithmeticException e) {
System.out.println("Exception caught in thread: " + e.getMessage());
}
});
thread.start();
Java multi-threading is a powerful tool that can significantly improve the performance of your applications. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can write robust and efficient multi-threaded code. However, multi-threading also introduces complexity and potential issues such as race conditions and deadlocks. Therefore, it is important to use multi-threading carefully and follow best practices to ensure the reliability of your applications.