Different ways to do multithreading in Java
Multithreading in Java allows for concurrent execution of multiple threads within a single program. This can greatly improve the performance and responsiveness of a program by allowing multiple tasks to be performed simultaneously. There are several ways to implement multithreading in Java, including the following:
Extending the Thread class: The simplest way to create a new thread in Java is to extend the Thread class and override its run() method. The run() method contains the code that will be executed in the new thread. Once the thread is created, it can be started by calling the start() method.
Extending the Thread class:
Trdexmp.java
import java.io.*;
import java.util.*;
class MyThread extends Thread {
public void run() {
for(int i=0; i<5; i++) {
System.out.println("Thread 1: " + i);
}
}
}
public class Trdexmp {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
}
}
Output:
Thread 1: 0
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
The MyThread class is defined by extending the Thread class and overriding its run() method. The run() method contains the code that will be executed in the new thread. In this case, a for loop is used to iterate 5 times and print "Thread 1: i" with i being the current iteration.
In the main method, an instance of the MyThread class is created and the start() method is called on it. This starts the execution of the thread, which will run the code in the run() method concurrently with the main thread.
Implementing the Runnable interface: Another way to create a new thread in Java is to implement the Runnable interface and override its run() method. The Runnable interface is more versatile than the Thread class, as it allows a class to implement other interfaces while still being able to be used as a thread. To start the thread, create a new Thread object and pass the Runnable object as a parameter to the constructor.
Implementing the Runnable interface:
Runexmp.java
import java.io.*;
import java.util.*;
class MyRunnable implements Runnable {
public void run() {
for(int i=0; i<5; i++) {
System.out.println("Thread 2: " + i);
}
}
}
public class Runexmp {
public static void main(String[] args) {
Thread t2 = new Thread(new MyRunnable());
t2.start();
}
}
Output:
Thread 2: 0
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
The MyRunnable class is defined by implementing the Runnable interface and overriding its run() method. The run() method contains the code that will be executed in the new thread. In this case, a for loop is used to iterate 5 times and print "Thread 2: i" with i being the current iteration.
In the main method, an instance of the Thread class is created and the MyRunnable class is passed as a parameter to the constructor. The start() method is called on the Thread instance, which starts the execution of the thread, which will run the code in the run() method concurrently with the main thread.
Using the Executor framework: The Executor framework is a higher-level way to manage threads in Java. It provides a way to execute tasks, manage thread pools, and handle thread scheduling. The framework includes several classes, such as Executor, ExecutorService, and ThreadPoolExecutor, which can be used to create and manage threads.
Using the Executor framework:
Exeemp.java
import java.io.*;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Exeemp{
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new Runnable() {
public void run() {
for(int i=0; i<5; i++) {
System.out.println("Thread 3: " + i);
}
}
});
executor.shutdown();
}
}
Output:
Thread 3: 0
Thread 3: 1
Thread 3: 2
Thread 3: 3
Thread 3: 4
The Executor framework is a higher-level way to manage threads in Java. It provides a way to execute tasks, manage thread pools, and handle thread scheduling.
In this example, the ExecutorService interface is used to create a single thread executor. The Executors.newSingleThreadExecutor() method is used to create the executor. The submit() method is used to submit a new task for execution and the task is represented by an instance of the Runnable interface. The run() method of this interface contains the code that will be executed in the new thread. In this case, a for loop is used to iterate 5 times and print "Thread 3: i" with i being the current iteration.
Once the task is submitted, the shutdown() method is called on the executor to shut it down. This will cause the executor to stop accepting new tasks and complete the existing ones. The Executor framework provides a way to manage threads by abstracting away the creation, management and destruction of threads. It also provides several other features like thread pooling and thread scheduling.
Using the Fork/Join framework: The Fork/Join framework is another way to perform multithreading in Java. It is designed for use in cases where a task can be broken down into smaller subtasks, which can be executed concurrently. The framework includes the ForkJoinPool class, which can be used to create and manage a pool of worker threads.
example of using the Fork/Join framework:
Forkexmp.java
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
class MyRecursiveTask extends RecursiveTask<Integer> {
private int[] array;
private int start;
private int end;
public MyRecursiveTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
protected Integer compute() {
if(end - start < 10) {
int sum = 0;
for(int i=start; i<end; i++) {
sum += array[i];
}
return sum;
} else {
int middle = (start + end) / 2;
MyRecursiveTask left = new MyRecursiveTask(array, start, middle);
MyRecursiveTask right = new MyRecursiveTask(array, middle, end);
left.fork();
right.fork();
return left.join() + right.join();
}
}
}
public class Forkexmp {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
ForkJoinPool pool = ForkJoinPool.commonPool();
MyRecursiveTask task = new MyRecursiveTask(array, 0, array.length);
int sum = pool.invoke(task);
System.out.println("Sum: " + sum);
}
}
Output:
Sum: 55
In this example, we have created a MyRecursiveTask class that extends the RecursiveTask<Integer> class. The compute() method is overridden to perform a recursive summation of an array of integers. The task is divided into smaller subtasks until the subtask is small enough to be executed sequentially. The ForkJoinPool.commonPool() is used to create a common pool of worker threads and the invoke() method is used to execute the task. The result of the task, the sum of the array, is printed to the console.
Using the Parallel Streams: Java 8 introduced the concept of parallel streams, which allow for concurrent execution of stream operations. Parallel streams use the fork/join framework to perform operations in parallel. To create a parallel stream, use the parallel() method provided by the stream.
Here's an example of using parallel streams in Java:
Prpexmp.java
import java.util.Arrays;
import java.util.List;
public class Prpexmp {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
numbers.stream().parallel().forEach(i -> System.out.println("Thread 5: " + i));
}
}
Output:
Thread 5: 0
Thread 5: 1
Thread 5: 2
Thread 5: 3
Thread 5: 4
However, the order in which the numbers are printed will be non-deterministic, meaning that the order may be different every time the program is run, as the elements are processed in parallel and the order in which they are processed is not guaranteed.
In this example, we have a list of integers and we are using the parallel() method provided by the stream to create a parallel stream. The forEach() method is used to perform an operation on each element of the stream, in this case, it is printing "Thread 5: i" with i being the current iteration.
As you may have noticed the output order may vary because parallel streams are non-deterministic, meaning that the order of the elements may not be the same as the original order. This is because the elements are processed in parallel and the order in which they are processed is not guaranteed.
Parallel streams use the fork/join framework to perform operations in parallel and this allows for better performance when working with large collections of data. It also provides a simple and easy-to-use way to perform concurrent operations on collections.
Each of these methods has its own advantages and disadvantages and can be used depending on the requirements of the specific application. It is important to consider the nature of the tasks being performed, the resources available, and the desired performance characteristics when choosing which method to use.