A thread is the smallest unit of execution in a program. In Java, a thread is an instance of the Thread
class or a class that extends it. Each thread has its own call stack, program counter, and local variables. Java programs start with a single thread called the main thread, and additional threads can be created to perform concurrent tasks.
Java threads can be in one of the following states:
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadClassExample {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
thread1.start();
thread2.start();
}
}
In this example, we create a class MyThread
that extends the Thread
class and overrides the run()
method. The run()
method contains the code that will be executed when the thread starts. We then create two instances of MyThread
and call the start()
method to start the threads.
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class RunnableExample {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable, "Thread 1");
Thread thread2 = new Thread(myRunnable, "Thread 2");
thread1.start();
thread2.start();
}
}
Here, we create a class MyRunnable
that implements the Runnable
interface and overrides the run()
method. We then create an instance of MyRunnable
and pass it to the Thread
constructor to create two threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running.");
}
}
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 5; i++) {
executor.submit(new Task());
}
executor.shutdown();
}
}
The ExecutorService
is a high - level framework for managing threads. In this example, we create a fixed - size thread pool using Executors.newFixedThreadPool(2)
. We then submit five tasks to the executor service, and finally, we call the shutdown()
method to gracefully shut down the executor service.
When multiple threads access shared resources, synchronization is necessary to prevent race conditions. Java provides the synchronized
keyword to achieve synchronization.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
class IncrementTask implements Runnable {
private Counter counter;
public IncrementTask(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
}
}
public class SynchronizationExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
IncrementTask task = new IncrementTask(counter);
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
In this example, the increment()
method of the Counter
class is declared as synchronized
, which means only one thread can execute this method at a time.
Java provides methods like wait()
, notify()
, and notifyAll()
for thread communication.
class ProducerConsumerExample {
private static final int MAX_SIZE = 5;
private java.util.LinkedList<Integer> buffer = new java.util.LinkedList<>();
class Producer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (buffer) {
while (buffer.size() == MAX_SIZE) {
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int item = (int) (Math.random() * 100);
buffer.add(item);
System.out.println("Produced: " + item);
buffer.notifyAll();
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (buffer) {
while (buffer.isEmpty()) {
try {
buffer.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int item = buffer.poll();
System.out.println("Consumed: " + item);
buffer.notifyAll();
}
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(example.new Producer());
Thread consumerThread = new Thread(example.new Consumer());
producerThread.start();
consumerThread.start();
}
}
In this producer - consumer example, the Producer
thread adds items to the buffer, and the Consumer
thread removes items from the buffer. The wait()
method is used to make a thread wait until a certain condition is met, and the notifyAll()
method is used to wake up all the waiting threads.
Deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources. To avoid deadlocks, follow these guidelines:
synchronized
blocks, use ReentrantLock.tryLock()
to avoid indefinite waiting.shutdown()
or shutdownNow()
method on the ExecutorService
to release resources.Java multithreading is a powerful feature that can significantly improve the performance and responsiveness of modern software applications. By understanding the fundamental concepts, using the appropriate usage methods, following common practices, and adhering to best practices, developers can harness the full potential of multithreading in Java. However, multithreading also introduces challenges such as race conditions and deadlocks, which need to be carefully managed. With proper knowledge and careful implementation, Java multithreading can be a valuable asset in software development.