11 KiB
Threads and Multithreading
Threads
A thread is a lightweight process. It is a unit of execution within a process. A process can have multiple threads. Each thread has its own program counter, stack, and registers. Threads share the same address space. This means that all threads in a process can access the same memory. This is different from processes where each process has its own address space.
Often, a process needs to perform multiple tasks at the same time. For example, a web browser needs to download a file and display a web page at the same time. Creating a new process for each task is expensive. This is because creating a new process requires a lot of resources.
Threads are used to solve this problem. Threads are used to perform multiple tasks within a process. This is done by sharing the same address space. This means that all threads in a process can access the same memory. This is different from processes where each process has its own address space.
Thread is a sequential flow of tasks within a process. Threads in OS can be of the same or different types. Threads are used to increase the performance of the applications. Each thread has its own program counter, stack, and set of registers. But the threads of a single process might share the same code and data/file. Threads are also termed as lightweight processes as they share common resources.
Thread vs Process
Process | Thread |
---|---|
Processes use more resources and hence they are termed as heavyweight processes. | Threads share resources and hence they are termed as lightweight processes. |
Creation and termination times of processes are slower. | Creation and termination times of threads are faster compared to processes. |
Processes have their own code and data/file. | Threads share code and data/file within a process. |
Communication between processes is slower. | Communication between threads is faster. |
Context Switching in processes is slower. | Context switching in threads is faster. |
Processes are independent of each other. | Threads, on the other hand, are interdependent. (i.e they can read, write or change another thread’s data) |
Eg: Opening two different browsers. | Eg: Opening two tabs in the same browser. |
Concurrency vs Parallelism
- Concurrent - At the same time, but not necessarily at the same instant. It is possible for multiple threads to be at different stages of execution at the same time but not being processed together. A single core CPU can only execute one thread at a time. But it can switch between threads very quickly. This is called context switching. This is how concurrency is achieved. A single core CPU can have concurrency but not parallelism.
- Parallel - At the same time and at the same instant. It is possible for multiple threads to be at different stages of execution at the same time and being processed together. A single core CPU cannot achieve parallelism. It can only achieve concurrency. A multi-core CPU can achieve both concurrency and parallelism.
Using threads in Java
In Java, we can create a thread by extending the Thread class or by implementing the Runnable interface. The Thread class is a subclass of the Object class. It implements the Runnable interface. The Runnable interface has a single method called run(). This method is called when the thread is started.
class NewThread implements Runnable {
@Override
public void run() {
// Code to be executed by the thread
}
}
We can create a new thread by creating an object of the NewThread class and passing it to the Thread class constructor. The Thread class constructor takes a Runnable object as an argument. This Runnable object is the thread that we want to create.
NewThread newThread = new NewThread();
Thread thread = new Thread(newThread);
To run the thread, we call the start() method on the Thread object. This method calls the run() method of the Runnable object. The run() method is executed by the thread.
thread.start();
Number printer
Problem Statement
- Create a new thread that prints the numbers from 1 to 10.
Solution
class NumberPrinter implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
}
}
public class Main {
public static void main(String[] args) {
NumberPrinter numberPrinter = new NumberPrinter();
Thread thread = new Thread(numberPrinter);
thread.start();
}
}
Problem Statement 2
- Print the numbers from 1 to 10 where each number is printed by a different thread.
Solution
class NumberPrinter implements Runnable {
private int number;
public NumberPrinter(int number) {
this.number = number;
}
@Override
public void run() {
System.out.println(number);
}
}
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
NumberPrinter numberPrinter = new NumberPrinter(i);
Thread thread = new Thread(numberPrinter);
thread.start();
}
}
}
Executor
The Executor
interface is used to execute tasks. It is a generic interface that can be used to execute any kind of task. The Executor
interface has only one method:
public interface Executor {
void execute(Runnable command);
}
The execute
method takes a Runnable
object as a parameter. The Runnable
interface is a functional interface that has only one method. Executors internally use a thread pool to execute the tasks. The execute
method is non-blocking. It returns immediately after submitting the task to the thread pool. The execute
method is used to execute tasks that do not return a result.
A thread pool is a collection of threads that are used to execute tasks. Instead of creating a new thread for each task, a thread pool reuses the existing threads to execute the tasks. This improves the performance of the application.
The Executor
interface has a method called newCachedThreadPool
that returns an ExecutorService
object. The ExecutorService
interface extends the Executor
interface. The ExecutorService
interface has methods to execute tasks that return a result. The ExecutorService
interface also has methods to shutdown the thread pool.
To run a task using the Executor
interface, we can use the newCachedThreadPool
method to create an ExecutorService
object. The newCachedThreadPool
method returns an ExecutorService
object that uses a thread pool with a variable number of threads. The newCachedThreadPool
method creates a new thread for each task if there are no idle threads in the thread pool. If there is an idle thread in the thread pool, the newCachedThreadPool
method reuses the idle thread to execute the task. The newCachedThreadPool
method returns an ExecutorService
object that uses a thread pool with a variable number of threads.
Executor executorService = Executors.newCachedThreadPool();
executorService.execute(() -> System.out.println("Hello World"));
Callable and Future
Runnables do not return a result. If we want to execute a task that returns a result, we can use the Callable
interface. The Callable
interface is a functional interface that has only one method:
public interface Callable<V> {
V call() throws Exception;
}
The call
method returns a result of type V
. The call
method can throw an exception. The Callable
interface is used to execute tasks that return a result.
For instance we can use the Callable
interface to execute a task that returns the sum of two numbers:
Callable<Integer> sumTask = () -> 2 + 3;
In order to execute a task that returns a result, we can use the submit
method of the ExecutorService
interface. The submit
method takes a Callable
object as a parameter. The submit
method returns a Future
object. The Future
interface has a method called get
that returns the result of the task. The get
method is a blocking method. It waits until the task is completed and then returns the result of the task.
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(() -> 2 + 3);
Integer result = future.get();
Futures can be used to cancel tasks. The Future
interface has a method called cancel
that can be used to cancel a task. The cancel
method takes a boolean parameter. If the boolean parameter is true
, the task is cancelled even if the task is already running. If the boolean parameter is false
, the task is cancelled only if the task is not running.
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Integer> future = executorService.submit(() -> 2 + 3);
future.cancel(false);
Assignment
- Create a count class that has a count variable.
- Create two different classes
Adder
andSubtractor
. - Accept a count object in the constructor of both the classes.
- In
Adder
, iterate from 1 to 100 and increment the count variable by 1 on each iteration. - In
Subtractor
, iterate from 1 to 100 and decrement the count variable by 1 on each iteration. - Print the final value of the count variable.
- What would the ideal value of the count variable be?
- What is the actual value of the count variable?
- Try to add some delay in the
Adder
andSubtractor
classes using inspiration from the code below. What is the value of the count variable now?
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}