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 the Thread
class or implements the Runnable
interface. Each thread has its own call stack, program counter, and local variables.
In Java, a thread can be in one of the following states:
The first way to create a thread in Java is by extending the Thread
class. Here is an example:
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 ThreadExample {
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 by the thread. We then create two instances of MyThread
and start them using the start()
method.
The second way to create a thread in Java is by implementing the Runnable
interface. Here is an example:
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 thread2 = new Thread(myRunnable);
thread1.start();
thread2.start();
}
}
In this example, 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. Finally, we start the threads using the start()
method.
The Executor framework is a high-level API in Java that simplifies the management of threads. It provides a thread pool that can be used to execute multiple tasks concurrently. Here is an example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyTask 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 ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
MyTask task = new MyTask();
executor.submit(task);
executor.submit(task);
executor.shutdown();
}
}
In this example, we create a class MyTask
that implements the Runnable
interface. We then create an executor service with a fixed thread pool of size 2. We submit two tasks to the executor service and finally shut down the executor service.
Synchronization is used to ensure that only one thread can access a shared resource at a time. In Java, we can use the synchronized
keyword to create synchronized blocks or methods. Here is an example:
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("Count: " + counter.getCount());
}
}
In this example, we create a class Counter
with a synchronized method increment()
. We then create a class IncrementTask
that implements the Runnable
interface and uses the Counter
object. We create two threads and start them. Finally, we wait for the threads to complete using the join()
method and print the count.
Thread communication is used to allow threads to communicate with each other. In Java, we can use the wait()
, notify()
, and notifyAll()
methods to achieve thread communication. Here is an example:
class Message {
private String msg;
private boolean empty = true;
public synchronized String read() {
while (empty) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
empty = true;
notifyAll();
return msg;
}
public synchronized void write(String msg) {
while (!empty) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
empty = false;
this.msg = msg;
notifyAll();
}
}
class Reader implements Runnable {
private Message message;
public Reader(Message message) {
this.message = message;
}
@Override
public void run() {
for (String msg = message.read(); !"Finished".equals(msg); msg = message.read()) {
System.out.println("Reader read: " + msg);
}
}
}
class Writer implements Runnable {
private Message message;
public Writer(Message message) {
this.message = message;
}
@Override
public void run() {
String[] messages = {"Hello", "World", "Finished"};
for (String msg : messages) {
message.write(msg);
System.out.println("Writer wrote: " + msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadCommunicationExample {
public static void main(String[] args) {
Message message = new Message();
Thread readerThread = new Thread(new Reader(message));
Thread writerThread = new Thread(new Writer(message));
readerThread.start();
writerThread.start();
}
}
In this example, we create a class Message
with two synchronized methods read()
and write()
. The read()
method waits until there is a message to read, and the write()
method waits until the message has been read. We then create a Reader
and a Writer
class that implement the Runnable
interface and use the Message
object. Finally, we create two threads and start them.
Using thread pools can help manage the number of threads in an application and reduce the overhead of creating and destroying threads. The Executor framework provides several types of thread pools, such as fixed thread pools, cached thread pools, and single-threaded executors.
Synchronization can introduce performance overhead and can lead to deadlocks if not used correctly. Try to use thread-safe data structures and algorithms that do not require synchronization.
Exceptions in threads can cause the thread to terminate unexpectedly. Make sure to handle exceptions properly in the run()
method of the thread.
Atomic variables are thread-safe variables that can be used to perform atomic operations without the need for synchronization. Java provides several atomic classes, such as AtomicInteger
, AtomicLong
, and AtomicBoolean
.
Java multi-threading is a powerful concept that can significantly enhance the performance of applications. In this blog post, we have explored the fundamental concepts of Java multi-threading, learned about its usage methods, common practices, and best practices. By following these practices, you can write efficient and reliable multi-threaded Java applications.