MultiThreading
MultiThreading
and Threads
Multithreading in Java
Thread Creation
- Subclass of Thread Class
- Using Runnable
Starting a Thread
Problem Statement 1 : Number Printer
Additional Concepts
1. Programs / Processes
Programs: These are sets of instructions for the computer. Each application you use, such as a
web browser or word processor, is a program.
Processes: When you open a program, it becomes a process. A process is an instance of a
program in execution.
2. Memory Allocation
When you start a program, the operating system (OS) allocates memory to it. This memory
contains the program's code, data, and other necessary information.
The Central Processing Unit (CPU) is the brain of the computer. It fetches instructions from
memory and executes them.
Each process takes turns using the CPU. The OS manages this by employing a technique called
CPU Scheduling.
4. Context Switching
The CPU rapidly switches between different processes. This is known as context switching.
The OS saves the current state of a process, loads the state of the next process, and hands
control to it.
5. Multitasking
6. Parallel Execution
In some systems, especially those with multiple processors or cores, true parallel execution can
occur. This means multiple processes genuinely run simultaneously.
7. Threads
A process can be further divided into threads. Threads within a process share the same
resources but can execute independently. Multithreading allows for parallel execution within a
single process.
8. Synchronization
When multiple processes or threads share resources (like data), synchronization mechanisms
are employed to avoid conflicts. This ensures that data remains consistent.We will discuss
synchronization in great detail in coming lectures.
The OS keeps track of all running processes and manages their execution.
It assigns priorities, allocates resources, and ensures fair access to the CPU. This is done by
scheduling algorithms. Some of the popular scheduling algorithms are as follows -
10. Interrupts
The CPU can be interrupted to handle external events, like input from a user or data arriving
from a network.Interrupts are crucial for maintaining responsiveness in a multitasking
environment.
When a program finishes its task or is closed by the user, the associated process is terminated.
The OS reclaims the allocated resources and frees up memory.
The goal is to efficiently utilize the available resources, ensuring that each running application
gets its fair share of CPU time. In summary, the execution of multiple applications or processes
involves careful management by the operating system, with the CPU rapidly switching between
tasks, allocating resources, and ensuring that everything runs smoothly. Multitasking and, in
some cases, parallel execution contribute to the efficiency and responsiveness of modern
computer systems.
Conclusion
Understanding how computer applications run involves grasping the intricacies of processes,
threads, CPU scheduling, multithreading, and parallel execution. As technology evolves,
mastering these concepts becomes increasingly important for developing efficient and
responsive applications. Experimenting with these concepts in programming languages and
frameworks will deepen your understanding and proficiency in building robust and high-
performance software.
Google Docs exemplifies the power of concurrency through its collaborative editing feature.
When multiple users are editing a document simultaneously, threads come into play. Each
user's edits are handled by a separate thread, ensuring that changes made by one user do not
disrupt the editing experience of others.
Threads in Google Docs
Thread per User: Each user's editing actions are processed by an individual thread.
Conflict Resolution: Threads synchronize to resolve conflicts and merge edits
seamlessly.
Auto-Suggest/Auto-complete: A separate thread can run spell check for the words you
write.
UI Thread: A separate thread can continuously update UI for the users.
In music players like Spotify or iTunes, threads are crucial for delivering a smooth user
experience during playback while allowing users to interact with the application concurrently.
In photo editing applications like Adobe Lightroom, where resource-intensive tasks like image
processing are common, threads are employed to maintain responsiveness and reduce
processing times.
Image Processing Threads: Multiple threads handle the processing of different parts of
an image concurrently.
Background Tasks: Threads enable background tasks like importing photos while
allowing users to continue editing.
Responsive UI: Threads ensure that the user interface remains responsive even during
computationally intensive operations.
A process is an independent program in execution. It has its own memory space called heap,
code, data, and system resources. The heap isn't shared between two applications or two
processes, they each have their own. The terms process and application are often used
interchangeably. Processes enable multiple tasks to run concurrently, offering isolation and
independence.
Process Lifecycle
Challenges
Key Differences
Concurrent Execution - Tasks may overlap in time but not necessarily execute
simultaneously.
Parallel Execution - Tasks are actively running at the same time on separate processors.
Resource Utilization
Hardware Requirement
Multithreading in Java
In the Java, multithreading is driven by the core concept of a Thread. There are two ways to
create Threads in Java.
Thread Class: Java provides the Thread class, which serves as the foundation for creating and
managing threads.
Runnable Interface: The Runnable interface is often implemented to define the code that a
thread will execute.
Lets write some logic that runs in a parallel thread by using the Thread framework. In the below
code example we are creating two threads and running them concurrently.
The above SimpleRunnable is just a task which we want to run in a separate thread.
There’re various approaches we can use for running it; one of them is to use the Thread class.
We create a new Thread and pass a Runnable as a lambda expression directly to its
constructor. The lambda expression defines the code to be executed in the new thread. In this
case, it's a simple prints message that prints the name of Thread.
1. Thread Initialization
If you have a class that extends the Thread class, or if you have a class that implements the
Runnable interface, you create an instance of that class, which represents the thread.
myThread.start();
2. Thread Scheduling:
The JVM's scheduler determines when the thread gets CPU time for execution. The actual
timing is managed by the operating system, and it may vary.
Once the thread is scheduled, the JVM calls the run method of the thread. The run method
contains the code that will be executed in the new thread.
4. Concurrent Execution:
If there are multiple threads in the program, they may execute concurrently, with each thread
running independently, potentially interleaving their execution.
5. Thread Termination:
The run method completes its execution, and the thread transitions to the "terminated" state.
The thread is no longer active.
Important Notes
Direct run Method Invocation: Calling the run method directly (myThread.run()) will not
start a new thread; it will execute the run method in the current thread (ie main
thread).
One-Time Execution: The start method can only be called once for a thread.
Subsequent calls will result in an IllegalThreadStateException.
In summary, calling thread.start() initiates the execution of a new thread, and the JVM
takes care of the thread scheduling and execution of the run method in a separate
concurrent context.
Problem Statement - 1 Number Printer
Write a program to print numbers from 1 to 100 using 100 different threads. Since you can't
control the order of execution of threads, it is okay to get these numbers in any order.
Hint: Create a Runnable Task, which prints a single number.
Solution
NumberPrinter.java
Main.java
Sample Output
In Java, the Thread class provides several commonly used methods for managing and
controlling threads. Here are some of the key methods:
1. start()
Initiates the execution of the thread, causing the run method to be called.
Usage myThread.start();
2. run()
Contains the code that will be executed by the thread. This method needs to be overridden
when extending the Thread class or implementing the Runnable interface.
Usage: Defined by the user based on the specific task.
3. sleep(long milliseconds)
Description: Causes the thread to sleep for the specified number of milliseconds, pausing its
execution.
Usage:Thread.sleep(1000);
4. join()
Waits for the thread to complete its execution before the current thread continues. It is often
used for synchronization between threads.
Usage: myThread.join();
5. interrupt()
Interrupts the thread, causing it to stop or throw an InterruptedException. The thread must
handle interruptions appropriately.
Usage:
myThread.interrupt();
6. isAlive():
Returns true if the thread has been started and has not yet completed its execution, otherwise
returns false.
Usage: boolean alive = myThread.isAlive();
7. setName(String name)
8. getName()
Returns the name of the thread.
Usage: String threadName = myThread.getName();
9. setPriority(int priority)
10. getPriority()
11. currentThread()
These methods provide essential functionality for managing thread execution, synchronization,
and interaction. When working with threads, it's crucial to understand and use these methods
effectively to create robust and efficient concurrent programs.
Solution
FactorialThread.java
import java.math.BigInteger;
FactorialThread(long number){
this.number = number;
result = BigInteger.valueOf(0); //Or BigInteger.ZERO;
isFinished = false;
}
@Override
public void run() {
//Business Logic
result = factorial(number);
isFinished = true;
}
BigInteger factorial(long n){
BigInteger ans = BigInteger.ONE;
for(long i=2; i<=n; i++){
ans = ans.multiply(BigInteger.valueOf(i));
}
return ans;
}
BigInteger getResult(){
return result;
}
boolean isFinished(){
return isFinished;
}
}
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
for(Thread t:threads){
t.start();
}
for(Thread t:threads){
t.join(2000);
}
//--------------------//
for(int i=0;i<inputNumbers.size();i++){
FactorialThread t = threads.get(i); //ith Thread Object
if(t.isFinished()){
System.out.println(t.getResult());
}
else{
System.out.println("Couldn't complete calc in 2s");
}
}
System.out.println("Main is completed!");
}
}
During thread lifecycle, threads go through various states. The java.lang.Thread class
contains a static State enum – which defines its potential states. During any given point of
time, the thread can only be in one of these states:
NEW – a newly created thread that has not yet started the execution
RUNNABLE – either running or ready for execution but it’s waiting for resource
allocation
BLOCKED – waiting to acquire a monitor lock to enter or re-enter a synchronized
block/method
WAITING – waiting for some other thread to perform a particular action without any
time limit
TIMED_WAITING – waiting for some other thread to perform a specific action for a
specified period
TERMINATED – has completed its execution
1.NEW
A NEW Thread (or a Born Thread) is a thread that’s been created but not yet started. It remains
in this state until we start it using the start() method.
The following code snippet shows a newly created thread that’s in the NEW state:
Since we’ve not started the mentioned thread, the method t.getState() prints:
NEW
2. Runnable
When we’ve created a new thread and called the start() method on that, it’s moved from NEW
to RUNNABLE state. Threads in this state are either running or ready to run, but they’re waiting
for resource allocation from the system.
For example, let’s add t.start() method to our previous code and try to access its current
state:
RUNNABLE
Note that in this example, it’s not always guaranteed that by the time our control reaches
t.getState(), it will be still in the RUNNABLE state.
It may happen that it was immediately scheduled by the Thread-Scheduler and may finish
execution. In such cases, we may get a different output.
3. BLOCKED
A thread is in the BLOCKED state when it’s currently not eligible to run. It enters this state when
it is waiting for a monitor lock and is trying to access a section of code that is locked by some
other thread.
Let’s try to reproduce this state:
t1.start();
t2.start();
In this code:
We’ve created two different threads – t1 and t2, t1 starts and enters the synchronized
commonResource() method; this means that only one thread can access it; all other
subsequent threads that try to access this method will be blocked from the further execution
until the current one will finish the processing.
When t1 enters this method, it is kept in an infinite while loop; this is just to imitate heavy
processing so that all other threads cannot enter this method
Now when we start t2, it tries to enter the commonResource() method, which is already being
accessed by t1, thus, t2 will be kept in the BLOCKED state.
Being in this state, we call t2.getState() and get the output as:
BLOCKED
4. WAITING
A thread is in WAITING state when it’s waiting for some other thread to perform a particular
action. According to JavaDocs, any thread can enter this state by calling any one of the
following three methods:
object.wait()
thread.join() or
LockSupport.park()
Note that in wait() and join() – we do not define any timeout period as that scenario is covered
in the next section.
In this example, thread-1 starts thread 2 and waits for thread-2 to finish using thread.join()
method. During this time t1 is in WAITING state.
@Override
public void run(){
Thread t2 = new Thread(new SimpleRunnableTwo());
t2.start();
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Main
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SimpleRunnable());
t1.start();
5. TIMED WAITING
A thread is in TIMED_WAITING state when it’s waiting for another thread to perform a particular
action within a stipulated amount of time.
According to JavaDocs, there are five ways to put a thread on TIMED_WAITING state:
thread.sleep(long millis)
wait(int timeout) or wait(int timeout, int nanos)
thread.join(long millis)
LockSupport.parkNanos
LockSupport.parkUntil
Here, we’ve created and started a thread t1 which is entered into the sleep state with a
timeout period of 5 seconds; the output will be TIMED_WAITING.
Thread.sleep(2000);
System.out.println(t1.getState());
}
}
6. TERMINATED
This is the state of a dead thread. It’s in the TERMINATED state when it has either finished
execution or was terminated abnormally. There are different ways of terminating a thread.
@Override
public void run() {
// No processing in this block
}
}
Here, while we’ve started thread t1, the very next statement Thread.sleep(1000) gives enough
time for t1 to complete and so this program gives us the output as:
TERMINATED
--End---
Concurrency-2 Executors and Callables
Executor Framework
Overview
Using Executor Framework
Thread Pools
Types of Thread Pool
Benefits of Executor Framework
Coding Problems
Adder - Subtractor
Overview
Imagine you have a computer program that needs to do several tasks at the same time. For
example, your program might need to download files, process data, and update a user
interface simultaneously. In the traditional way of programming, you might use threads to
handle these tasks. However, managing threads manually can be complex and error-prone.
Java ExecutorService implementations let you stay focused on tasks that need to be run, rather
than thread creation and management.
Think of a chef in a kitchen as your program. The chef has multiple tasks like chopping
vegetables, cooking pasta, and baking a cake. Instead of the chef doing each task one by one,
the chef hires sous-chefs (threads) to help. The chef (Executor Framework) can assign tasks to
sous-chefs efficiently, ensuring that multiple tasks are happening simultaneously, and the
kitchen operates smoothly.
In Java, the Executor Framework provides a convenient way to implement this idea in your
code, making it more readable, maintainable, and efficient. It simplifies the process of
managing tasks concurrently, so you can focus on solving the problems your program is
designed to address.
Now, let's look at a real-world example to illustrate the use of the Executor Framework.
Consider a scenario where you have a set of tasks that need to be executed concurrently to
improve performance. We'll use a ThreadPoolExecutor for this example:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Here NumberPrinter() is runnable task as created earlier. 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:
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.
Creating threads, destroying threads, and then creating them again can be expensive.
A thread pool mitigates the cost, by keeping a set of threads around, in a pool, for
current and future work.
Threads, once they complete one task, can then be reassigned to another task, without
the expense of destroying that thread and creating a new one.
1. Worker Threads are available in a pool to execute tasks. They're pre-created and kept
alive, throughout the lifetime of the application.
2. Submitted Tasks are placed in a First-In First-Out queue. Threads pop tasks from the
queue, and execute them, so they're executed in the order they're submitted.
3. The Thread Pool Manager allocates tasks to threads, and ensures proper thread
synchronization.
In Java, the ExecutorService interface, along with the ThreadPoolExecutor class, provides a
flexible thread pool framework. Here are five types of thread pools in Java, each with different
characteristics:
1. FixedThreadPool
Description: A thread pool with a fixed number of threads.
Characteristics:
Creation:
2. CachedThreadPool
Description: A thread pool that dynamically adjusts the number of threads based on
demand.
Characteristics
3. SingleThreadExecutor
Description: A thread pool with only one thread.
Characteristics:
4.ScheduledThreadPool
Description: A thread pool that supports scheduling of tasks.
Characteristics:
Similar to FixedThreadPool but with added support for scheduling tasks at fixed
rates or delays.
Suitable for periodic tasks or tasks that need to be executed after a certain
delay.
5. WorkStealingPool
Description: Introduced in Java 8, it's a parallelism-friendly thread pool.
Characteristics:
These different types of thread pools cater to various scenarios and workloads. The choice of a
thread pool depends on factors such as task characteristics, execution requirements, and
resource constraints in your application.
The Executor Framework makes it easier to execute tasks concurrently. It abstracts away the
low-level details of managing threads, so you don't have to worry about creating and
controlling them yourself.
When you have many tasks to perform, creating a new thread for each task can be inefficient.
The Executor Framework provides a pool of threads that can be reused for multiple tasks. This
reuse of threads reduces the overhead of creating and destroying threads for every task.
With the Executor Framework, you can control how many tasks can run simultaneously,
manage the lifecycle of threads, and specify different policies for task execution. This level of
control is important for optimizing the performance of your program.
4. Enhanced Scalability
When your program needs to handle more tasks, the Executor Framework makes it easier to
scale. You can adjust the size of the thread pool to accommodate more tasks without rewriting
a lot of code.
5. Task Scheduling
The framework allows you to schedule tasks to run at specific times or after certain intervals.
This is useful for scenarios where you want to automate repetitive tasks or execute tasks at
specific points in time.
Summary
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:
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.
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.
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
}
@Override
public List<Integer> call() throws Exception {
//Business Logic
//base case
if(arr.size()<=1){
return arr;
}
//recursive case
int n = arr.size();
int mid = n/2;
leftArr = leftFuture.get();
rightArr = rightFuture.get();
//Merge
List<Integer> output = new ArrayList<>();
int i=0;
int j=0;
while(i<leftArr.size() && j<rightArr.size()){
if(leftArr.get(i)<rightArr.get(j)){
output.add(leftArr.get(i));
i++;
}
else{
output.add(rightArr.get(j));
j++;
}
}
// copy the remaining elements
while(i<leftArr.size()){
output.add(leftArr.get(i));
i++;
}
while(j<rightArr.size()){
output.add(rightArr.get(j));
j++;
}
return output;
}
}
Main.java
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class DownloadManager {
private ExecutorService executorService;
Solution
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Override
public void run() {
// Simulate file download
System.out.println("Downloading file from: " + fileUrl);
class DownloadManager {
private ExecutorService executorService;
downloadManager.downloadFiles(filesToDownload);
downloadManager.shutdown();
}
}
Solution
Repainting a 2D array using four threads can be achieved by dividing the array into quadrants,
and assigning each quadrant to a separate thread for repainting.
This example divides the 2D array into four quadrants and assigns each quadrant to a separate
thread for repainting. The ArrayRepainterTask class represents the task for repainting a specific
quadrant. The program then uses an ExecutorService with a fixed thread pool to concurrently
execute the tasks. Finally, it prints the repainted 2D array.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Override
public void run() {
// Simulate repainting for the specified quadrant
for (int i = startRow; i <= endRow; i++) {
for (int j = startCol; j <= endCol; j++) {
array[i][j] = array[i][j] * 2; // Repaint by doubling the
values (simulated)
}
}
}
}
Synchronisation
Whenever we have multiple threads that access the same resource, we need to make sure that
the threads do not interfere with each other. This is called synchronisation.
Synchronisation can be seen in the adder and subtractor example. The adder and subtractor
threads access the same counter variable. If the adder and subtractor threads do not
synchronise, the counter variable can be in an inconsistent state.
Adder
package addersubtractor;
public class Adder implements Runnable {
private Count count;
public Adder (Count count) {
this.count = count;
}
@Override
public void run() {
for (int i = 1 ; i <= 100; ++ 1) {
count.value += i;
}
}
Subtracter
package addersubtractor;
public class Subtractor implements Runnable {
private Count count;
public Subtractor (Count count) {
this.count = count
}
@Override
public void run() {
for (int i = 1 ; i <= 100; ++ 1) {
count.value -= i;
}
}
Count class
package addersubtractor;
public class Count {
int value = 0;
}
system.out.println(count.value);
Synchronisation Problem
Adder Subtracter Recap
The adder and subtractor problem is a sample problem that is used to demonstrate the need
for synchronisation in a system. The problem is as follows:
Create a count class that has a count variable.
Create two different classes Adder and Subtractor.
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 and Subtractor classes using
inspiration from the code below. What is the value of the count variable now?
Adder.java
public class Adder implements Runnable {
private Count count;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count.increment();
}
}
}
Subtracter.java
public class Subtractor implements Runnable {
private Count count;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
count.decrement();
}
}
}
Runner.java
public class Runner {
public static void main(String[] args) {
Count count = new Count();
Adder adder = new Adder(count);
Subtractor subtractor = new Subtractor(count);
adderThread.start();
subtractorThread.start();
adderThread.join();
subtractorThread.join();
System.out.println(count.getCount());
}
}
Synchronisation Problem
In multithreaded environments, synchronization problems can arise due to concurrent
execution of multiple threads, leading to unpredictable and undesirable behavior. There are
certain conditions that can lead to synchronization problems. These Conditions are:
1. Critical Section - A critical section is a part of the code that must be executed by only
one thread at a time to avoid data inconsistency or corruption. - If multiple threads access
and modify shared data simultaneously within a critical section, it can lead to unpredictable
results. - Ensuring mutual exclusion by using synchronization mechanisms like locks or
semaphores helps prevent multiple threads from entering the critical section simultaneously.
Race Condition:
2. Race Conditions - A race condition occurs when the final outcome of a program
depends on the relative timing of events, such as the order in which threads are scheduled
to run. - In a race condition, the correctness of the program depends on the timing of the
thread executions, and different outcomes may occur depending on the interleaving of
thread execution. - Proper synchronization mechanisms, like locks or atomic operations,
are needed to prevent race conditions by enforcing a specific order of execution for critical
sections. Preemption:
3. Preemption - Preemption refers to the interrupting of a currently executing thread to
start or resume the execution of another thread. - In multithreaded environments,
preemption can lead to issues if not handled carefully. For example, a thread might be
preempted while in the middle of updating shared data, leading to inconsistent or corrupted
state. - To avoid issues related to preemption, critical sections should be protected using
mechanisms like locks or disabling interrupts temporarily to ensure that a thread completes
its operation without being interrupted.
To address these synchronization problems, various synchronization mechanisms are
employed, such as locks, semaphores, and atomic operations. These tools help ensure that
only one thread can access critical sections at a time, preventing race conditions and
mitigating the impact of preemption on shared data. Additionally, proper design practices,
like minimizing the use of shared mutable data and using thread-safe data structures, can
contribute to reducing synchronization issues in multithreaded environments.
2. Progress:
Definition: The overall system should keep moving and making progress. It
should not stop at any stage and be waiting for a long period. If no thread is in its
critical section and some threads are waiting to enter the critical section, then the
selection of the next thread to enter the critical section should be definite.
Importance: Guarantees that the system makes progress and avoids deadlock
situations where threads are unable to proceed.
3. Bounded Waiting:
Definition: There exists a limit on the number of times other threads are allowed
to enter their critical sections after a thread has requested entry into its critical
section and before that request is granted. No thread should be waiting infinitely.
There should be a bound on how long they have to wait before they are allowed to
enter the critical section.
Importance: Prevents the problem of starvation, where a thread is repeatedly
delayed in entering its critical section by other threads.
4. No Deadlock:
Definition: A deadlock is a state where two or more threads are blocked forever,
each waiting for the other to release a resource.
Importance: A good synchronization solution should avoid deadlocks, as they can
lead to a complete system halt and result in unresponsive behavior.
5. Efficiency:
Definition: The synchronization solution should introduce minimal overhead and
allow non-conflicting threads to execute concurrently.
Importance: Ensures that the system performs well and doesn’t suffer from
unnecessary delays or resource contention.
6. Adaptability:
Definition: The synchronization solution should be adaptable to different system
configurations and workloads.
Importance: Facilitates the use of the synchronization mechanism in a variety of
scenarios without requiring significant modifications.
7. Low Busy-Waiting:
Definition: Minimizes the use of busy-waiting (spinning in a loop while waiting for
a condition to be satisfied) to conserve CPU resources. When a thread has to
continuously check if they can now enter the critical section. Checking if a thread
can enter the critical section is not a productive use of time. The ideal solution
should have some kind of notification system. For example if you have to check if
a person is available or not: In way 1, you go and knock on the person’s door
every 2 minutes to check if they are free. This is busy waiting In way 2, you go
and tell the person that I am here. Please let me know when you are free. This is
called a notification. This provides better usage of the time.
Importance: Reduces unnecessary CPU consumption, making the system more
efficient and avoiding the negative impact of busy-waiting on power consumption.
8. Fairness:
Definition: All threads should have a fair chance to enter their critical sections. No
thread should be unfairly delayed or granted preferential access.
Importance: Ensures that the synchronization solution treats all threads fairly,
preventing situations where some threads consistently get better access to shared
resources.
9. Scalability:
Definition: The synchronization solution should scale well with an increasing
number of threads and resources.
Importance: Allows the system to efficiently handle a growing number of threads
without a significant degradation in performance.
10. Portability:
Definition: The synchronization solution should be portable across different
platforms and operating systems.
Importance: Enables the synchronization mechanism to be used in diverse
computing environments without requiring extensive modifications.
## Solutions to Synchronisation
Problem
### Coding Problem 1 - Thread Safe
Counter (Homework) // Implement a
class that represents a counter and is
accessed by multiple threads. // Ensure
that the counter is updated in a thread-
safe manner without using the
synchronized keyword.
```java public class ThreadSafeCounter {
private int count = 0;
// TODO: Implement a thread-safe
method to increment the counter.
public static void main(String[] args) { //
TODO: Create multiple threads that
concurrently increment the counter. //
Ensure that the counter is updated in a
thread-safe manner without using the
synchronized keyword. } } ```
### Coding Problem 2 - Reentrantlock
Basics (Homework) Implement a
program that uses ReentrantLock to
achieve thread safety.
```java import
java.util.concurrent.locks.Lock; import
java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private int value = 0; private final Lock
lock = new ReentrantLock();
// TODO: Implement a method to update
the value using ReentrantLock.
public static void main(String[] args) { //
TODO: Create multiple threads that
concurrently update the value using
ReentrantLock. // Ensure that the value is
updated in a thread-safe manner. } } ```
Additonal Topics
1. Atomic Datatypes in java
In Java, the java.util.concurrent.atomic package provides a set of classes that support
atomic operations on variables. These classes are designed to be thread-safe and eliminate
the need for explicit synchronization in certain scenarios. One commonly used class is
AtomicInteger. In this tutorial, we’ll explore AtomicInteger and provide a simple code
example.
Atomic Integer Basics The AtomicInteger class provides atomic operations on an integer
variable. These operations are performed in a way that ensures atomicity, making them
thread-safe without the need for explicit synchronization.
Example Usage:
import java.util.concurrent.atomic.AtomicInteger;
Adder.java
public class Adder implements Runnable{
private InventorCounter ic;
Adder(InventorCounter ic){
this.ic = ic;
}
@Override
public void run() {
for(int i=0;i<=10000;i++){
ic.counter.addAndGet(1);
}
}
}
Subtracter.java
public class Subtracter implements Runnable{
private InventorCounter ic;
Subtracter(InventorCounter ic){
this.ic = ic;
}
@Override
public void run() {
for(int i=0;i<=10000;i++){
ic.counter.addAndGet(-1);
}
}
}
Main.java
public class Main {
public static void main(String[] args) throws InterruptedException {
t1.join();
t2.join();
System.out.println(ic.counter.get());
}
}
In summary, the AtomicInteger class in Java provides a convenient and efficient way to
perform atomic operations on integer variables, making it a valuable tool for concurrent
programming. Similar classes, such as AtomicLong and AtomicBoolean, exist for other
primitive types.
2. Volatile Keyword
Video Tutorial on Volatile
Volatile Keyword solves for problems like Memory Inconsistency Errors & Data Races.
Let’s understand this in more detail.
The Operating system may read from heap variables, and make a copy of the value in each
thread’s own storage. Each threads has its own small and fast memory storage, that holds
its own copy of shared resource’s value.
Once thread can modify a shared variable, but this change might not be immediately
reflected or visible. Instead it is first update in thread’s local cache. The operating system
may not flush the first thread’s changes to the heap, until the thread has finished executing,
causing memory inconsistency errors.
Main.java
public class Main {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
System.out.println("Shared Resource Created, Flag Value " + sharedResource
}
System.out.println("In Thread B, Flag is "+sharedResource.getFlag
});
A.start();
B.start();
}
}
Solution - Volatile Keyword - The volatile keyword is used as modifier for class
variables. - It’s an indicator that this variable’s value may be changed by multiple threads. -
This modifier ensures that the variable is always read from, and written to the main
memory, rather than from any thread-specific cache. - This provides memory consistency
for this variables value across threads. Volatile doesn’t gurantee atomicicty.
However, volatile does not provide atomicity or synchronization, so additional
synchronization mechanisms should be used in conjunction with it when necessary.
When to use volatile - When a variable is used to track the state of a shared resource,
such as counter or a flag. - When a varaible is used to communicate between threads.
When not use volatile - When the variable is used by single thread. - When a variable is
used to store a large amount of data. ### 3. Concurrent Data Structures There are data
structures designed in Collections Framework which support Concurrency but we will
limit our discussions to one of the widely asked data structures - Concurrent Hashmap.
Java Collections provides various data structures for working with key-value pairs. The
commonly used ones are - - Hashmap (Non-Synchronised, Not Thread Safe) - discuss the
Synchronized Hashmap method
Hashtable (Synchronised, Thread Safe)
locking over entire table
Concurrent Hashmap (Synchronised, Thread Safe, Higher Level of Concurrency,
Faster)
locking at bucket level, fine grained locking
The synchronizedMap() is a static method of the Collections class that takes an instance
of HashMap collection as a parameter and returns a synchronized Map from it. However,it
is important to note that only the map itself is synchronized, not its views such as keyset
and entrySet. Therefore, if we want to iterate over the synchronized map, we need to use a
synchronized block or a lock to ensure exclusive access.
import java.util.*;
public class Maps {
public static void main(String[] args) {
HashMap<String, Integer> cart = new HashMap<>();
// Adding elements in the cart map
cart.put("Butter", 5);
cart.put("Milk", 10);
cart.put("Rice", 20);
cart.put("Bread", 2);
cart.put("Peanut", 2);
// printing synchronized map from HashMap
Map mapSynched = Collections.synchronizedMap(cart);
System.out.println("Synchronized Map from HashMap: " + mapSynched);
}
}
ConcurrentHashMap, on the other hand, provides thread safety with a higher level of
concurrency. It allows multiple threads to read and perform limited writes simultaneously
without locking the entire data structure. This is especially useful in applications that
have more read operations than write operations.
Performance Comparison Hashtable locks the entire table during a write operation,
thereby preventing other reads or writes. This could be a bottleneck in a high-concurrency
environment.
ConcurrentHashMap, however, allows concurrent reads and limited concurrent writes,
making it more scalable and often faster in practice.
class ReservationSystem {
private int availableSeats;
private final Lock lock = new ReentrantLock();
user1.start();
user2.start();
user3.start();
try {
user1.join();
user2.join();
user3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
In this example, the ReservationSystem class utilizes a Reentrant Lock (lock) to ensure that
the reservation process is thread-safe. The reserveSeats method is enclosed in a try-finally
block to ensure that the lock is always released, even if an exception occurs. This real-
world problem demonstrates how Reentrant Locks can be used to synchronize access to
shared resources in a multi-threaded environment, ensuring data consistency and
preventing race conditions in a scenario like an online reservation system.
Coding Problem 4 - Thread-safe Bank Transactions
Problem Statement: You are tasked with implementing a simple bank system that
supports concurrent transactions. The bank has multiple accounts, and customers can
deposit and withdraw money from their accounts concurrently. Implement a program that
ensures the integrity of bank transactions by using threads. Requirements: - Each
account has a unique account number and an initial balance. - Customers can concurrently
deposit and withdraw money from their accounts. - The bank should ensure that the
account balance remains consistent and does not go below zero during concurrent
transactions. - Use threads to simulate multiple customers performing transactions
simultaneously.
Solution
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BankAccount {
private final int accountNumber;
private int balance;
private final Lock lock = new ReentrantLock();
@Override
public void run() {
// Simulate a bank transaction (deposit or withdrawal)
if (transactionAmount >= 0) {
account.deposit(transactionAmount);
} else {
account.withdraw(Math.abs(transactionAmount));
}
}
}
thread1.start();
thread2.start();
thread3.start();
try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
In this example, BankAccount represents a bank account with deposit and withdraw
methods protected by a ReentrantLock. The BankTransaction class simulates a bank
transaction (deposit or withdrawal), and the BankSimulation class demonstrates how
threads can be used to perform concurrent transactions on multiple accounts. The use of
locks ensures the thread safety of the bank transactions
Additonal Reading
Reentrant Locks
Concurrent DataStructures
Fairness of Re-entrant Locks
– End –
# Concurrency-4 Synchronization with Semaphores
Agenda
Synchronisation using Semaphores
Producer Consumer Problem using Semaphores
Producer Consumer Problem using Concurrent Data Structure (Queue)
Print In Order LeetCode Problem
Deadlocks
Additional Topics
wait(), notify() methods
Producer Consumer Using wait() & notify()
Coding Projects (Optional)
Traffic Intersection Control
Resource Pooling in Library
LeetCode Problems
Additional Resources
// Produce a T-shirt
System.out.println("Producer produces a T-shirt. Total T-shirts: "
// Consume a T-shirt
System.out.println("Consumer buys a T-shirt. Total T-shirts: "
producerThread.start();
consumerThread.start();
}
}
Semaphores
mutex: Controls access to the critical sections (mutex stands for mutual exclusion).
empty: Represents the number of empty slots in the store, initially set to the store’s
capacity.
full: Represents the number of filled slots in the store, initially set to 0.
Producer:
The producer acquires an empty slot using empty.acquire() and enters the critical
section with mutex.acquire().
It produces a T-shirt, increments the count, releases the mutex, and signals that a T-
shirt is ready for consumption using full.release().
Consumer:
The consumer acquires a filled slot using full.acquire() and enters the critical section
with mutex.acquire().
It consumes a T-shirt, decrements the count, releases the mutex, and signals that an
empty slot is available for production using empty.release().
Simulated Production and Consumption: Thread.sleep() is used to simulate the time
it takes to produce and consume T-shirts. Execution: When you run this program, you will
observe the producer producing T-shirts and the consumer buying T-shirts. The store’s
capacity is maintained, and semaphores ensure proper synchronization between the
producer and the consumer.
This example demonstrates how semaphores can be used to solve the Producer-Consumer
problem efficiently, preventing issues such as overproduction or stockouts.
Java Implementation -2 using Semaphores Producer.java
public class Producer implements Runnable{
private Queue<Object> queue;
private int maxSize;
private String name;
private Semaphore producerSemaphore;
private Semaphore consumerSemaphore;
Consumer.java
package Multithreading.ProducerConsumer;
import java.util.Queue;
import java.util.concurrent.Semaphore;
@Override
public void run() {
while(true){
try {
consumerSemaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(queue.size()>0){
System.out.println(this.name + " removing from queue, Size "
queue.remove();
}
producerSemaphore.release();
}
}
}
Client.java
Here we create multiple producers and multiple consumers.
public class Client {
public static void main(String[] args) {
Queue<Object> objects = new ConcurrentLinkedQueue<>();
int maxSize = 6;
Semaphore producerSemaphore = new Semaphore(maxSize);
Semaphore consumerSemaphore = new Semaphore(0);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
}
}
@Override
public void run() {
//Each producer wants to continuously produces
// T-Shirts and add them to the queue if there is space available
while(true){
synchronized (queue){
if(queue.size()<this.maxSize){
System.out.println("Adding - "+ queue.size());
queue.add(new Object());
}
}
}
}
}
Consumer.java
import java.util.Queue;
@Override
public void run() {
//Each producer wants to continuously produces
// T-Shirts and add them to the queue if there is space available
while(true){
synchronized (queue) {
if (queue.size() > 0) {
System.out.println("Removing - "+ queue.size());
queue.remove();
}
}
}
}
}
Main.java
public class Main {
public static void main(String[] args) {
//Shared Object
Queue<Object> q = new ConcurrentLinkedQueue<>();
int maxSize = 6;
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
t8.start();
}
}
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
semaSecond.release();
}
public void second(Runnable printSecond) throws InterruptedException {
semaSecond.acquire();
printSecond.run();
semaThird.release();
}
public void third(Runnable printThird) throws InterruptedException {
semaThird.acquire();
printThird.run();
}
}
DeadLocks
A deadlock in OS is a situation in which more than one process is blocked because it is
holding a resource and also requires some resource that is acquired by some other process.
Deadlock
Process P1 and P2 are in a deadlock because: * Resources are non-shareable. (Mutual
exclusion) * Process 1 holds “Resource 1” and is waiting for “Resource 2” to be released
by process 2. (Hold and wait) * None of the processes can be preempted. (No preemption)
* “Resource 1” and needs “Resource 2” from Process 2 while Process 2 holds “Resource
2” and requires “Resource 1” from Process 1. (Circular wait)
Tackling deadlocks
There are three ways to tackle deadlocks: * Prevention - Implementing a mechanism to
prevent the deadlock. * Avoidance - Avoiding deadlocks by not allocating resources when
deadlocks are possible. * Detecting and recovering - Detecting deadlocks and recovering
from them. * Ignorance - Ignore deadlocks as they do not happen frequently.
Deadlock prevention means to block at least one of the four conditions required for
deadlock to occur. If we are able to block any one of them then deadlock can be
prevented. Spooling and non-blocking synchronization algorithms are used to prevent the
above conditions. In deadlock prevention all the requests are granted in a finite amount of
time.
In Deadlock avoidance we have to anticipate deadlock before it really occurs and ensure
that the system does not go in unsafe state.It is possible to avoid deadlock if resources are
allocated carefully. For deadlock avoidance we use Banker’s and Safety algorithm for
resource allocation purpose. In deadlock avoidance the maximum number of resources of
each type that will be needed are stated at the beginning of the process.
We let the system fall into a deadlock and if it happens, we detect it using a detection
algorithm and try to recover.
Some ways of recovery are as follows:
Aborting all the deadlocked processes.
Abort one process at a time until the system recovers from the deadlock.
Resource Preemption: Resources are taken one by one from a process and assigned to
higher priority processes until the deadlock is resolved.
3. Ignorance
The system assumes that deadlock never occurs. Since the problem of deadlock situation
is not frequent, some systems simply ignore it. Operating systems such as UNIX and
Windows follow this approach. However, if a deadlock occurs we can reboot our system
and the deadlock is resolved automatically.
Set timeouts for all the processes. If a process does not respond within the timeout
period, it is killed.
Implementing with caution: Use interfaces that handle or provide callbacks if locks are
held by other processes.
Add timeout to locks: If a process requests a lock, and it is held by another process, it
will wait for the lock to be released until the timeout expires.
Additonal Topics
Inter-thread Communication using wait() and notify()
wait() & notify() - Recording Link Certainly! In Java, the wait() and notify() methods are
part of the built-in mechanism for inter-thread communication and synchronization. These
methods are used to coordinate the activities of multiple threads, allowing them to work
together effectively. Let’s break down these concepts for beginners:
wait() Method:
The wait() method is called on an object within a synchronized context (i.e., within a
method or block synchronized on that object).
It causes the current thread to release the lock on the object and enter a state of
waiting. Purpose:
wait() is used when a thread needs to wait for a certain condition to be met before
proceeding.
For example, if a thread is waiting for a shared resource to be available, it can call wait()
until another thread notifies it that the resource is ready. Example:
synchronized (sharedObject) {
while (!conditionMet) {
try {
sharedObject.wait(); // Releases the lock and waits for notification
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// Continue with the critical section
}
notify() Method:
Important Points: - Both wait() and notify() must be called within a synchronized context
to avoid illegal monitor state exceptions. - The calling thread must hold the lock on the
object on which it is calling wait() or notify(). - The wait() method releases the lock,
allowing other threads to access the synchronized block or method. - The notify() method
signals a waiting thread to wake up, allowing it to reacquire the lock and continue
execution.
Example Scenario: Consider a scenario where multiple threads are working on a shared
resource. If a thread finds that the resource is not yet available (e.g., a buffer is empty), it
can call wait() to release the lock and wait until another thread populates the buffer and
calls notify() to signal that the resource is ready for consumption.
In summary, wait() and notify() are fundamental methods for thread synchronization in
Java, enabling threads to communicate and coordinate their activities efficiently.
synchronized (this) {
System.out.println("Take t-shirt? ");
sc.nextLine();
System.out.println("Recieved T-shirt");
notify();
Thread.sleep(3000);
}
}
}
PCDemo.java
public class PCDemo {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
t1.start();
t2.start();
}
}
class SharedBuffer {
private final LinkedList<Integer> buffer = new LinkedList<>();
private final int capacity;
@Override
public void run() {
try {
while (true) {
sharedBuffer.produce();
Thread.sleep(1000); // Simulate production time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
try {
while (true) {
sharedBuffer.consume();
Thread.sleep(1500); // Simulate consumption time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
producerThread.start();
consumerThread.start();
}
}
In this example:
SharedBuffer is the shared buffer where the producer produces and the consumer
consumes items.
The Producer class produces items and adds them to the buffer.
The Consumer class consumes items from the buffer.
The main method creates instances of the shared buffer, producer, and consumer, and
starts their respective threads.
This solution uses the wait() and notify() methods to ensure that the producer
waits when the buffer is full and the consumer waits when the buffer is empty,
allowing for proper coordination and synchronization between the two threads.
Coding Projects
Coding Problem 1 : Traffic Intersection Control
Implement a program that simulates a traffic intersection control system using Java
Semaphores. The intersection has two roads, each with its own traffic signal. The traffic
lights control the flow of traffic through the intersection. Requirements: - There are two
roads, Road A and Road B, crossing at the intersection. - Each road has its own traffic
signal (Semaphore) controlling the traffic flow. - The traffic lights for Road A and Road B
have a cycle of Green, Yellow, and Red signals. Only one road should have a green light at
a time, while the other road has a red light. The intersection should allow a smooth
transition between green lights for both roads.
Constraints: - The time duration for each signal (Green, Yellow, Red) can be adjusted
based on the program’s design. - Use Semaphores to control access to the traffic signals
and ensure a safe transition. Each road signal should run in a separate thread. - Implement
a way to visually represent the current state of the traffic signals and indicate which road
has the green light.
Sample Output:
Road A: Green Road B: Red
[… Some time passes …]
Road A: Yellow Road B: Red
[… Some time passes …]
Road A: Red Road B: Green
Notes: - The program should demonstrate proper synchronization to ensure that only one
road has a green light at any given time. - You may choose to implement additional features
such as a pedestrian signal or a button for road switching. - Consider the safety and
efficiency of the intersection control system. - This problem statement reflects a real-world
scenario where semaphores can be used to control access to shared resources (in this
case, the green light for each road) in a concurrent environment. Students can implement
the solution to gain hands-on experience with semaphore-based synchronization in a
practical setting.
Solution:
import java.util.concurrent.Semaphore;
class TrafficIntersectionControl {
class TrafficIntersectionControl {
private Semaphore roadASemaphore = new Semaphore(1); // Semaphore for Road A's traffic signal
private Semaphore roadBSemaphore = new Semaphore(0); // Semaphore for Road B's traffic signal
try {
roadASemaphore.acquire(); // Acquire the semaphore for Road A
roadBSemaphore.release(); // Release the semaphore for Road B
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Road A: Yellow");
sleep(2); // Yellow light duration
System.out.println("Road A: Red");
switchLights(); // Switch to Road B
}
}
System.out.println("Road B: Green");
sleep(5); // Green light duration
System.out.println("Road B: Yellow");
sleep(2); // Yellow light duration
System.out.println("Road B: Red");
switchLights(); // Switch to Road A
}
}
roadAThread.start();
roadBThread.start();
}
}
Coding Problem - 2 Real-Life Problem Statement: Resource Pooling in a Library
Imagine a library that has a collection of books, and multiple students want to borrow and
return books from the library. Implement a program that uses Java Semaphores to control
access to the books, ensuring that the available resources (books) are used efficiently.
Requirements: The library has a fixed number of books (resources) available for
borrowing. Students can borrow books from the library and return them after reading. The
library enforces a limit on the maximum number of students who can borrow books
simultaneously. When a student returns a book, another student can borrow it if there is an
available slot.
Constraints: Use Semaphores to control access to the shared resource (books). Each
student should run in a separate thread. The program should handle the borrowing and
returning of books concurrently. Implement a way to visually represent the current state of
the library, indicating which books are borrowed and available.
Example Output:
Student 1 borrows Book A Library: [Book A is borrowed, Book B is available, Book C is
available]
Student 2 borrows Book B Library: [Book A is borrowed, Book B is borrowed, Book C is
available]
Student 1 returns Book A Library: [Book A is available, Book B is borrowed, Book C is
available]
… Notes: Ensure that the library’s state is properly synchronized to avoid race conditions.
You may choose to implement additional features, such as a waitlist for students. Consider
scenarios where a student may need to wait if all books are currently borrowed. This
problem statement reflects a real-world scenario where semaphores can be used to control
access to a limited set of resources, ensuring that they are utilized efficiently and
concurrently by multiple entities. Students can implement the solution to gain practical
experience with semaphores in a resource pooling scenario.
Sample Code
import java.util.concurrent.Semaphore;
class Library {
private static final int MAX_STUDENTS = 2;
private static final int MAX_BOOKS = 3;
// Create and start multiple threads for students using the library
for (int i = 1; i <= MAX_STUDENTS; i++) {
int finalI = i;
new Thread(() -> library.student(finalI)).start();
}
}
}
Additonal Resources
Udemy - Concurrency, Multithreading and Parallel Computing in Java
Memory Management in Operating Systems - Thrasing, Paging etc Interview Topics
– End –