0% found this document useful (0 votes)
2 views

MultiThreading

This tutorial introduces the concepts of processes and threads in computer applications, emphasizing the importance of concurrency and multithreading. It explores real-world applications like Google Docs, music players, and Adobe Lightroom, showcasing how threads enhance performance and responsiveness. Additionally, it covers multithreading in Java, including thread creation, lifecycle, and common methods, while addressing the benefits and challenges of multithreading.

Uploaded by

shane
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

MultiThreading

This tutorial introduces the concepts of processes and threads in computer applications, emphasizing the importance of concurrency and multithreading. It explores real-world applications like Google Docs, music players, and Adobe Lightroom, showcasing how threads enhance performance and responsiveness. Additionally, it covers multithreading in Java, including thread creation, lifecycle, and common methods, while addressing the benefits and challenges of multithreading.

Uploaded by

shane
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 63

Concurrency-1 Introduction to Processes

and Threads

In this tutorial, we will cover the following concepts.

How Computer Applications Run

Concurrency: Real-World Applications of Threads


- Google Docs
- Music Player
- Adobe Lightroom

Processes and Threads


- Benefits of Multithreading
- Challenges of Multithreading

Concurrent vs Parallel Execution

Multithreading in Java

Thread Creation
- Subclass of Thread Class
- Using Runnable
Starting a Thread
Problem Statement 1 : Number Printer

Additional Concepts

Commonly used Methods on Threads


Problem Statement 2: Factorial Computation Task
Thread Lifecycle & States

Understanding How Computer Applications Run


Computer applications are complex systems that run on a computer's operating system,
interacting with hardware and software components to perform various tasks. To comprehend
how these applications operate efficiently, it's essential to delve into fundamental concepts like
processes, threads, CPU scheduling, multithreading, and parallel execution. Let us understand
how a program runs.

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.

3. Processor (CPU) Execution

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

The ability of a computer to execute multiple processes concurrently is called multitasking.


While it may seem like everything is happening at once, the CPU is actually rapidly switching
between processes.

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.

9. Task Management by the Operating System

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 -

First-Come-First-Serve (FCFS): Processes are executed in the order they arrive.


Shortest Job Next (SJN): The process with the shortest execution time is selected.
Round Robin (RR): Each process gets a fixed time slice, then moves to the back of the
queue.
Priority Scheduling: Processes are assigned priorities, and the highest priority process is
executed first.
Modern CPUs often employ a mix of static and dynamic scheduling strategies.
Advanced techniques, like predictive algorithms, may be used to anticipate the next process to
run.

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.

11. Termination of Processes

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.

12. Efficient Resource Utilization

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.

Concurrency: Real-World Applications of Threads


Concurrent programming, which involves the execution of multiple tasks simultaneously, is a
fundamental concept in modern software development. One powerful mechanism for achieving
concurrency is the use of threads. Let's explore how threads are employed in real-world
applications, focusing on Google Docs, Music Players, and Adobe Lightroom.

Concurrent programming enables multiple operations to progress in overlapping time intervals.


Threads, the smallest units of execution within a process, are instrumental in achieving
concurrency. They allow different parts of a program to run concurrently, enhancing efficiency
and responsiveness.

1. Google Docs: Collaborative Editing

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.

2. Music Players: Smooth Playback and User Interaction

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.

How Threads Work in Music Players

Playback Thread: A dedicated thread manages audio playback, ensuring uninterrupted


streaming.
User Interface Thread: Another thread handles user interactions, such as browsing
playlists or adjusting settings.
Parallel Execution: Threads allow simultaneous playback and user interactions without
one affecting the other.

3. Adobe Lightroom: Image Processing

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.

How Threads Work in Lightroom

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.

Processes and Threads - Deep Dive

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

Creation: When a program is launched, it is loaded into memory, a process is created.


Execution: The process runs its instructions.
Termination: The process completes its execution or is terminated.
A thread is the smallest unit of execution within a process. Multiple threads can exist within a
single process, sharing the same resources like memory but executing independently.

Benefits of Multithreading - Why use multiple threads?

Performance: Threads can execute concurrently, enhancing performance.


Responsiveness: Multithreading allows a program to remain responsive during time-
consuming tasks. This is especially helpful in applications with user interfaces.
Efficiency: Exploiting parallelism improves overall system performance.
One of the most common reasons, is to offload long running tasks.
Instead of tying up the main thread, we can create additional threads, to execute tasks
that might take a long time. This frees up the main thread so that it can continue
working, and executing, and being responsive to the user.
You also might use multiple threads to process large amounts of data, which can
improve performance, of data intensive operations.
A web server, is another use case for many threads, allowing multiple connections and
requests to be handled, simultaneously.
Resource Sharing: Threads within a process share resources, reducing overhead.

Challenges

Data Synchronization: Threads may need to synchronize access to shared data to


prevent conflicts.
Deadlocks: Concurrent threads might lead to situations where each is waiting for the
other to release a resource.

We will address these challenges in the up-coming classes.

Concurrent Execution vs Parallel Execution


Concurrent execution refers to the ability of a system to execute multiple tasks or
processes at the same time, appearing to overlap in time. Concurrent Execution can
happen on single core as well.

Parallel execution involves the simultaneous execution of multiple tasks or processes


using multiple processors or cores. Multiple cores are must for truly parallel execution.

Data Parallelism: Dividing a task into subtasks processed concurrently.


Task Parallelism: Assigning multiple independent tasks to separate processors/cores.

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

Concurrent Execution - Utilizes a single processor by interleaving tasks.


Parallel Execution - Utilizes multiple processors, ensuring more tasks are completed in the
same time frame.

Hardware Requirement

Concurrent Execution - Can occur on a system with a single processor.


Parallel Execution - Requires multiple processors or cores.
Example

Concurrent Execution - Multiple applications running on a single-core processor.


Parallel Execution - Image processing tasks being performed simultaneously on different
cores of a multi-core processor.

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.

Way-1 : Subclassing a Thread Class

public class NewThread extends Thread {


public void run() {
// business logic
...
}
}
}

Class to initialize and start our thread.

public class MultipleThreadsExample {


public static void main(String[] args) {
NewThread t1 = new NewThread();
t1.setName("MyThread-1");
NewThread t2 = new NewThread();
t2.setName("MyThread-2");
t1.start();
t2.start();
}
}

Way -2 : Using Runnable (Preferred Way)

class SimpleRunnable implements Runnable {


public void run() {
// business logic
}
}
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new SimpleRunnable());
t.start();
}
}

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.

Simply put, we generally encourage the use of Runnable over Thread:


When extending the Thread class, we’re not overriding any of its methods. Instead, we override
the method of Runnable (which Thread happens to implement).

This is a clear violation of IS-A Thread principle


Creating an implementation of Runnable and passing it to the Thread class utilizes
composition and not inheritance – which is more flexible
After extending the Thread class, we can’t extend any other class
From Java 8 onwards, Runnables can be represented as lambda expressions

public class ThreadWithLambdaExample {


public static void main(String[] args) {
// Creating a thread with a Runnable implemented as a lambda expression
Thread myThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
...
}
});

// Starting the thread


myThread.start();
}
}

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.

Starting a Thread - Behind the Scenes


When you call thread.start() in Java, it initiates the execution of the thread and invokes the
run method of the thread. Here's a step-by-step explanation of what happens:

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.

Thread myThread = new MyThread(); // or Thread myThread = new Thread(new


MyRunnable());
The thread is in the "new" state after initialization. When you call start(), the thread transitions
to the "runnable" state. It is ready to run but is waiting for its turn to be scheduled by the Java
Virtual Machine (JVM).

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.

3. run() Method Execution:

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.

class MyThread extends Thread {


public void run() {
// Code to be executed by the thread
}
}

If you implemented Runnable instead:

class MyRunnable implements Runnable {


public void run() {
// Code to be executed by the 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

public class NumberPrinter implements Runnable {


int number;
NumberPrinter(int number){
this.number = number;
}
@Override
public void run(){
System.out.println("Printing "+number + " from
"+Thread.currentThread().getName());
}
}

Main.java

public class Main {


public static void main(String[] args) {
for(int i=0; i<100;i++){
Thread t = new Thread(new NumberPrinter(i));
t.start();
}
}
}

Sample Output

Printing 3 from Thread-3


Printing 19 from Thread-19
Printing 14 from Thread-14
Printing 6 from Thread-6
Printing 21 from Thread-21
Printing 22 from Thread-22
Printing 0 from Thread-0
Printing 10 from Thread-10
...
Printing 94 from Thread-94
Printing 95 from Thread-95
Printing 96 from Thread-96
Printing 97 from Thread-97
Printing 98 from Thread-98
Printing 99 from Thread-99
Additonal Concepts (Optional)
Lets cover some more advanced concepts related to Threads.

Commonly used Methods on Threads

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)

Sets the name of the thread.


Usage: myThread.setName("MyThread");

8. getName()
Returns the name of the thread.
Usage: String threadName = myThread.getName();

9. setPriority(int priority)

Sets the priority of the thread. Priorities range from Thread.MIN_PRIORITY to


Thread.MAX_PRIORITY.
Usage: myThread.setPriority(Thread.MAX_PRIORITY);

10. getPriority()

Returns the priority of the thread.


Usage: int priority = myThread.getPriority();

11. currentThread()

Returns a reference to the currently executing thread object.


Usage: Thread currentThread = Thread.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.

Problem Statement - 2 Factorial Computation Task


Write a program that computes Factorial of a list of numbers. Each factorial should be
computed on a separate thread. For each factorial calculation, do not wait for more than 2
seconds.
Hint: Use the join() method on each factorial thread, before main starts executing again.

Solution

FactorialThread.java
import java.math.BigInteger;

public class FactorialThread extends Thread {


private long number;
private BigInteger result;
private boolean isFinished;

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;

public class Main {

// Task calculate Factorial of List of Numbers


public static void main(String[] args) throws InterruptedException {

List<Long> inputNumbers = Arrays.asList(100000000L, 3435L, 35435L,


2324L, 4656L, 23L, 5556L);
List<FactorialThread> threads = new ArrayList<>();
for(long number:inputNumbers){
FactorialThread t = new FactorialThread(number);
//System.out.println(t.getState());
threads.add(t);
}

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!");
}
}

Thread Life Cycle

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:

Runnable runnable = new NewState();


Thread t = new Thread(runnable);
System.out.println(t.getState());

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.

In a multi-threaded environment, the Thread-Scheduler (which is part of JVM) allocates a fixed


amount of time to each thread. So it runs for a particular amount of time, then leaves the
control to other RUNNABLE threads.

For example, let’s add t.start() method to our previous code and try to access its current
state:

Runnable runnable = new NewState();


Thread t = new Thread(runnable);
t.start();
System.out.println(t.getState());

This code is most likely to return the output as:

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:

public class BlockedState {


public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new DemoBlockedRunnable());
Thread t2 = new Thread(new DemoBlockedRunnable());

t1.start();
t2.start();

Thread.sleep(1000); //pause so that t2 states changes during this time


System.out.println(t2.getState());
System.exit(0);
}
}

class DemoBlockedRunnable implements Runnable {


@Override
public void run() {
commonResource();
}

public static synchronized void commonResource() {


while(true) {
// Infinite loop to mimic heavy processing
// 't1' won't leave this method
// when 't2' try to enter this
}
}
}

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.

Simple Runnable.java - Thread 1

public class SimpleRunnable implements Runnable{

@Override
public void run(){
Thread t2 = new Thread(new SimpleRunnableTwo());
t2.start();
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

Simple Runnable 2 - Thread 2

public class SimpleRunnableTwo implements Runnable {


@Override
public void run() {
try{
Thread.sleep(5000);
}
catch(InterruptedException e){
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}

Main
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SimpleRunnable());
t1.start();

Thread.sleep(1000); //1ms pause


System.out.println("T1 :"+ t1.getState()); //T1 is waiting state
System.out.println("Main :" + Thread.currentThread().getState());
}
}

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.

public class SimpleRunnable implements Runnable{


@Override
public void run() {
try{
Thread.sleep(5000);
}
catch(InterruptedException e){
e.printStackTrace();
}
}
}

In Main, if you check the state of T1 after 2s it will be TIMED WAITING

public class Main {


public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SimpleRunnable());
t1.start();

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.

Let’s try to achieve this state in the following example:

public class TerminatedState implements Runnable {


public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TerminatedState());
t1.start();

// The following sleep method will give enough time for


// thread t1 to complete
Thread.sleep(1000);
System.out.println(t1.getState());
}

@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

In this tutorial, we will cover the following concepts.

Executor Framework

Overview
Using Executor Framework
Thread Pools
Types of Thread Pool
Benefits of Executor Framework

Callables & Future

How threads can return data?

Coding Problems

Multi-threaded Merge Sort


Download Manager
Image Processing App
Scheduled Executor

Synchronization Problems Introduction

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.

Using Executor Framework


The Executor Framework in Java provides a high-level and flexible framework for managing and
controlling the execution of tasks in concurrent programming. It is part of the
java.util.concurrent package and was introduced in Java 5 to simplify the development of
concurrent applications. The primary motivation behind using the Executor Framework is to
abstract away the complexities of thread management, providing a clean and efficient way to
execute tasks asynchronously.

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;

public class ExecutorDemo {


public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for(int i=0; i<100;i++){
executor.execute(new NumberPrinter(i));
}
executor.shutdown();
}
}

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:

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.

Thread Pool - Deep Dive

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.

A thread pool consists of three components.

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.

Types of Thread Pool

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:

Reuses a fixed number of threads for all submitted tasks.


If a thread is idle, it will be reused for a new task.
If all threads are busy, tasks are queued until a thread becomes available.

Creation:

ExecutorService executor = Executors.newFixedThreadPool(nThreads);

2. CachedThreadPool
Description: A thread pool that dynamically adjusts the number of threads based on
demand.
Characteristics

Creates new threads as needed, but reuses idle threads if available.


Threads that are idle for a certain duration are terminated.
Suitable for handling a large number of short-lived tasks.

ExecutorService executor = Executors.newCachedThreadPool();

3. SingleThreadExecutor
Description: A thread pool with only one thread.
Characteristics:

Executes tasks sequentially in the order they are submitted.


Useful for tasks that need to be executed in a specific order or when a single
thread is sufficient.

ExecutorService executor = Executors.newSingleThreadExecutor();

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.

ScheduledExecutorService executor = Executors.newScheduledThreadPool(nThreads);

5. WorkStealingPool
Description: Introduced in Java 8, it's a parallelism-friendly thread pool.
Characteristics:

Creates a pool of worker threads that dynamically adapt to the number of


available processors.
Each worker thread has its own task queue.
Suitable for parallel processing tasks.

ExecutorService executor = Executors.newWorkStealingPool();

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.

Benefits of Executor Framework


1. Simplifies Task Execution

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.

2. Efficient Resource Utilization:

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.

3. Better Control and Flexibility

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

Managing threads manually can be complex and error-prone.


It can lead to complex issues like resource contention, thread creation overhead, and
scalability challenges.
For these reasons, you'll want to use an ExecutorService, even when working with a
single thread.

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);

Coding Problem 1 : Merge Sort


Implement multi-threaded merge sort.
Solution
Sorter.java

import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

public class Sorter implements Callable<List<Integer>> {


private List<Integer> arr;
private ExecutorService executor;
Sorter(List<Integer> arr,ExecutorService executor){
this.arr = arr;
this.executor = executor;

}
@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;

List<Integer> leftArr = new ArrayList<>();


List<Integer> rightArr = new ArrayList<>();

//Division of array into 2 parts


for(int i=0;i<n;i++){
if(i<mid){
leftArr.add(arr.get(i));
}
else{
rightArr.add(arr.get(i));
}
}

//Recursively Sort the 2 array


Sorter leftSorter = new Sorter(leftArr,executor);
Sorter rightSorter = new Sorter(rightArr,executor);

Future<List<Integer>> leftFuture = executor.submit(leftSorter);


Future<List<Integer>> rightFuture = executor.submit(rightSorter);

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;

public class Main {


public static void main(String[] args) throws Exception {
List<Integer> l = List.of(7,3,1,2,4,6,17,12);
ExecutorService executorService = Executors.newCachedThreadPool();

Sorter sorter = new Sorter(l,executorService);


Future<List<Integer>> output = executorService.submit(sorter);
System.out.println(output.get()); //Blocking Code
executorService.shutdown();
}
}

Coding Problem 2 : Download Manager (Homework)


Consider a simple download manager application that needs to download multiple files
concurrently. Implement the download manager using the Java Executor Framework.
Requirements:
The download manager should be able to download multiple files simultaneously.
Each file download is an independent task that can be executed concurrently.
The download manager should use a thread pool from the Executor Framework to
manage and execute the download tasks.
Implement a mechanism to track the progress of each download task and display it to
the user.

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class DownloadManager {
private ExecutorService executorService;

public DownloadManager(int threadPoolSize) {


// TODO: Initialize the ExecutorService with a fixed-size thread pool.
}

public void downloadFiles(List<String> fileUrls) {


// TODO: Implement a method to submit download tasks for each file URL.
}

// TODO: Implement a method to track and display the progress of each


download task.

public void shutdown() {


// TODO: Shutdown the ExecutorService when the download manager is
done.
}
}

public class DownloadManagerApp {


public static void main(String[] args) {
// TODO: Create a DownloadManager instance with an appropriate thread
pool size.
// TODO: Test the download manager by downloading multiple files
concurrently.
// TODO: Display the progress of each download task.
// TODO: Shutdown the download manager after completing the downloads.
}
}

Tasks for Implementation

Initialize the ExecutorService in the DownloadManager constructor.


Implement the downloadFiles method to submit download tasks for each file URL using
the ExecutorService.
Implement a mechanism to track and display the progress of each download task.
Test the download manager in the DownloadManagerApp by downloading multiple files
concurrently.
Shutdown the ExecutorService when the download manager is done.
Feel free to adapt and extend the code as needed. This example focuses on using the
Executor Framework for concurrent file downloads in a download manager application.

Solution

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class DownloadTask implements Runnable {


private String fileUrl;

public DownloadTask(String fileUrl) {


this.fileUrl = fileUrl;
}

@Override
public void run() {
// Simulate file download
System.out.println("Downloading file from: " + fileUrl);

// Simulate download progress


for (int progress = 0; progress <= 100; progress += 10) {
System.out.println("Progress for " + fileUrl + ": " + progress +
"%");
try {
Thread.sleep(500); // Simulate download time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

System.out.println("Download complete for: " + fileUrl);


}
}

class DownloadManager {
private ExecutorService executorService;

public DownloadManager(int threadPoolSize) {


executorService = Executors.newFixedThreadPool(threadPoolSize);
}

public void downloadFiles(List<String> fileUrls) {


for (String fileUrl : fileUrls) {
DownloadTask downloadTask = new DownloadTask(fileUrl);
executorService.submit(downloadTask);
}
}

public void shutdown() {


executorService.shutdown();
}
}

public class DownloadManagerApp {


public static void main(String[] args) {
DownloadManager downloadManager = new DownloadManager(3); // Use a
thread pool size of 3

List<String> filesToDownload = List.of("file1", "file2", "file3",


"file4", "file5");

downloadManager.downloadFiles(filesToDownload);

// Display progress (simulated)


// Note: In a real-world scenario, you might need to implement a more
sophisticated progress tracking mechanism.
for (int i = 0; i < 10; i++) {
System.out.println("Main thread is doing some work...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

downloadManager.shutdown();
}
}

Coding Problem 3 : Image Processing


Many image processing applications like Lightroom & Photoshop use multiple threads to
process an image quickly. In this problem, you will build a simplified image repainting task
using multiple threads, the repainting task here simply doubles the value of every pixel stored
in the form of a 2D array. Take Input a NXN matrix and repaint it by using 4 threads, one for
each quadrant.

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.

Below is an example code using the Java Executor Framework

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ArrayRepainterTask implements Runnable {


private final int[][] array;
private final int startRow;
private final int endRow;
private final int startCol;
private final int endCol;

public ArrayRepainterTask(int[][] array, int startRow, int endRow, int


startCol, int endCol) {
this.array = array;
this.startRow = startRow;
this.endRow = endRow;
this.startCol = startCol;
this.endCol = endCol;
}

@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)
}
}
}
}

public class ArrayRepaintingExample {


public static void main(String[] args) {
int[][] originalArray = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};

int rows = originalArray.length;


int cols = originalArray[0].length;

ExecutorService executorService = Executors.newFixedThreadPool(4);

// Divide the array into four quadrants


int midRow = rows / 2;
int midCol = cols / 2;

// Create tasks for each quadrant


ArrayRepainterTask task1 = new ArrayRepainterTask(originalArray, 0,
midRow - 1, 0, midCol - 1);
ArrayRepainterTask task2 = new ArrayRepainterTask(originalArray, 0,
midRow - 1, midCol, cols - 1);
ArrayRepainterTask task3 = new ArrayRepainterTask(originalArray,
midRow, rows - 1, 0, midCol - 1);
ArrayRepainterTask task4 = new ArrayRepainterTask(originalArray,
midRow, rows - 1, midCol, cols - 1);

// Submit tasks to the ExecutorService


executorService.submit(task1);
executorService.submit(task2);
executorService.submit(task3);
executorService.submit(task4);

// Shutdown the ExecutorService


executorService.shutdown();

// Wait for all tasks to complete


while (!executorService.isTerminated()) {
// Wait
}

// Print the repainted array


for (int[] row : originalArray) {
for (int value : row) {
System.out.print(value + " ");
}
System.out.println();
}
}
}

Coding Problem 4: Scheduled Executor


Write a Java program that uses ScheduledExecutorService to schedule a task to run
periodically. Implement a task that prints a message “Hello” at fixed intervals of 5s.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorExample {

public static void main(String[] args) {


// Create a ScheduledExecutorService with a single thread
ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();

// Schedule the task to run periodically every 5 seconds


scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Hello");
}, 0, 5, TimeUnit.SECONDS);

// Sleep for a while to allow the task to run multiple times


try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}

// Shutdown the ScheduledExecutorService


scheduledExecutorService.shutdown();
}
}

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.

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 10000 and increment the count variable by 1 on each
iteration.
In Subtractor, iterate from 1 to 10000 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?
Steps to implement

Implement Adder & Subtractor


Shared counter via constructor
Create a package called addersubtractor
Create two tasks under adder and subtractor

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;
}

Now, let’s make our client class here:


public static void main(String[] args) {
Count count = new Count;
Adder adder = new Adder (count);
Subtractor subtractor = new Subtractor (count);
Thread t1 = new Thread (adder);
Thread t2 = new Thread (subtractor);
t1.start();
t2.start();
t1.join();
t2.join();

system.out.println(count.value);

Output is some random number every time we run the code.

Now, this particular problem is known to be a data synchronization problem.


This happens because the same data object is shared among various multi-threads, and they
both are trying to modify the same data. This is an unexpected result that we have seen, but
we will continue this in the next tutorial.
# Concurrency-3 Introduction to Synchronisation,
Mutex, Synchronized, Atomic Data-types
Agenda
Synchronisation Problem
Adder Subtracter Recap
Conditions for Synchronisation Problem
Properties for a Good Solution
Solutions for Synchronisation
Mutex Locks
Synchronised Keyword
Semaphores(Next Class)
Coding Problems
Thread Safe Counter
ReentrantLock Basics
Addtional Topics
Atomic Datatypes
Volatile Keyword
Concurrent Hashmap (Interviews)
Coding Projects (Homework)
Ticket Booking System (Project)
Thread Safe Bank Transactions
Additional Reading

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;

public Adder(Count count) {


this.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;

public Subtractor(Count count) {


this.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);

Thread adderThread = new Thread(adder);


Thread subtractorThread = new Thread(subtractor);

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.

Properties of a Good Synchronization Solution


1. Mutual Exclusion:
Definition: Only one thread should be allowed to execute its critical section at any
given time. Suppose there are three threads, and they are waiting to enter the
critical sections of the Adder, Subtractor, and Multiplier. But a blocker should be
there to allow only one thread in a critical section at a time.
Importance: Ensures that conflicting operations on shared resources do not occur
simultaneously, preventing data corruption or inconsistency.

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;

public class AtomicIntegerExample {

public static void main(String[] args) {


// Create an AtomicInteger with an initial value
AtomicInteger atomicInteger = new AtomicInteger(0);

// Perform atomic increment


int newValue = atomicInteger.incrementAndGet();
System.out.println("Incremented Value: " + newValue);

// Perform atomic decrement


newValue = atomicInteger.decrementAndGet();
System.out.println("Decremented Value: " + newValue);

// Perform atomic add


int addValue = 5;
newValue = atomicInteger.addAndGet(addValue);
System.out.println("After Adding " + addValue + ": " + newValue);

// Perform compare-and-set operation


int expectedValue = 5;
int updateValue = 10;
boolean success = atomicInteger.compareAndSet(expectedValue, updateValue
if (success) {
System.out.println("Value updated successfully. New Value: " +
} else {
System.out.println("Value was not updated. Current Value: " + atomicInteger
}
}
}

Benefits of Atomic Datatypes: - Thread Safety: Operations on AtomicInteger are atomic,


eliminating the need for explicit synchronization. - Performance: Atomic operations are
more efficient than using locks for simple operations on shared variables. - Simplicity:
Simplifies the development of thread-safe code in scenarios where simple atomic
operations suffice.
Lets use Atomic Integers in Adder-Subtracter Example. InventoryCounter.java
public class InventorCounter {
AtomicInteger counter = new AtomicInteger(0);
}

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 {

InventorCounter ic = new InventorCounter();


Thread t1 = new Thread(new Adder(ic));
Thread t2 = new Thread(new Subtracter(ic));
t1.start();
t2.start();

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.

Lets see it through this code in action: SharedResourced.java


public class SharedResource {
volatile private boolean flag;
SharedResource(){
flag = false;
}
//Two More Methods
public void toggleFlag(){
flag = !flag;
}
public boolean getFlag(){
return flag;
}
}

Main.java
public class Main {
public static void main(String[] args) {
SharedResource sharedResource = new SharedResource();
System.out.println("Shared Resource Created, Flag Value " + sharedResource

Thread A = new Thread(()->{


//After 2S, toggle the value
try{
Thread.sleep(2000);
}
catch(InterruptedException e){
e.printStackTrace();
}
sharedResource.toggleFlag();
System.out.println("Thread A is finished, Flag is "+sharedResource
});

Thread B = new Thread(()->{


while(!sharedResource.getFlag()){
//...busy-wait...
// System.out.println("Inside Loop " + sharedResource.getFlag());

}
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

Hashmap and Synchronised Hashmap Method Synchronization is the process of


establishing coordination and ensuring proper communication between two or more
activities. Since a HashMap is not synchronized which may cause data inconsistency,
therefore, we need to synchronize it. The in-built method ‘Collections.synchronizedMap()’
is a more convenient way of performing this task.
A synchronized map is a map that can be safely accessed by multiple threads without
causing concurrency issues. On the other hand, a Hash Map is not synchronized which
means when we implement it in a multi-threading environment, multiple threads can access
and modify it at the same time without any coordination. This can lead to data
inconsistency and unexpected behavior of elements. It may also affect the results of an
operation.
Therefore, we need to synchronize the access to the elements of Hash Map using
‘synchronizedMap()’. This method creates a wrapper around the original HashMap and
locks it whenever a thread tries to access or modify it.
Collections.synchronizedMap(instanceOfHashMap);

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);
}
}

Hashtable vs Concurrent Hashmap HashMap is generally suitable for single threaded


applications and is faster than Hashtable, however in multithreading environments we have
you use Hashtable or Concurrent Hashmap. So let us talk about them.
While both Hashtable and Concurrent Hashmap collections offer the advantage of thread
safety, their underlying architectures and capabilities significantly differ. Whether we’re
building a legacy system or working on modern, microservices-based cloud applications,
understanding these nuances is critical for making the right choice.
Let’s see the differences between Hashtable and ConcurrentHashMap, delving into their
performance metrics, synchronization features, and various other aspects to help us make
an informed decision.
1. Hashtable Hashtable is one of the oldest collection classes in Java and has been present
since JDK 1.0. It provides key-value storage and retrieval APIs:
Hashtable<String, String> hashtable = new Hashtable<>();
hashtable.put("Key1", "1");
hashtable.put("Key2", "2");
hashtable.putIfAbsent("Key3", "3");
String value = hashtable.get("Key2");
The primary selling point of Hashtable is thread safety, which is achieved through
method-level synchronization.
Methods like put(), putIfAbsent(), get(), and remove() are synchronized. Only one thread
can execute any of these methods at a given time on a Hashtable instance, ensuring data
consistency.
2. Concurrent Hashmap ConcurrentHashMap is a more modern alternative, introduced
with the Java Collections Framework as part of Java 5.
Both Hashtable and ConcurrentHashMap implement the Map interface, which accounts for
the similarity in method signatures:
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap
concurrentHashMap.put("Key1", "1");
concurrentHashMap.put("Key2", "2");
concurrentHashMap.putIfAbsent("Key3", "3");
String value = concurrentHashMap.get("Key2");

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.

Coding Projects on Synchronisatoin


Coding Problem 3 - Ticket Booking System
Consider an online reservation system for booking tickets to various events. The system
needs to handle concurrent requests from multiple users trying to reserve seats. To ensure
thread safety and prevent race conditions, a Reentrant Lock can be employed.
Requirements: - The reservation system manages the availability of seats for different
events. - Multiple users can attempt to reserve seats concurrently. - A user should be able
to reserve multiple seats for the same event. - The system should prevent overbooking and
ensure the integrity of seat reservations.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ReservationSystem {
private int availableSeats;
private final Lock lock = new ReentrantLock();

public ReservationSystem(int totalSeats) {


this.availableSeats = totalSeats;
}

public void reserveSeats(String user, int numSeats) {


lock.lock();
try {
if (numSeats > 0 && numSeats <= availableSeats) {
// Simulate the reservation process
System.out.println(user + " is reserving " + numSeats + " seats."

// Update available seats


availableSeats -= numSeats;

// Simulate the ticket issuance


System.out.println(user + " reserved seats successfully.");
} else {
System.out.println(user + " could not reserve seats. Not enough available seats."
}
} finally {
lock.unlock();
}
}

public int getAvailableSeats() {


return availableSeats;
}
}

public class OnlineReservationSystem {


public static void main(String[] args) {
ReservationSystem reservationSystem = new ReservationSystem(50);

// Simulate multiple users trying to reserve seats concurrently


Thread user1 = new Thread(() -> reservationSystem.reserveSeats("User1"
Thread user2 = new Thread(() -> reservationSystem.reserveSeats("User2"
Thread user3 = new Thread(() -> reservationSystem.reserveSeats("User3"

user1.start();
user2.start();
user3.start();

try {
user1.join();
user2.join();
user3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

System.out.println("Remaining available seats: " + reservationSystem


}
}

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();

public BankAccount(int accountNumber, int initialBalance) {


this.accountNumber = accountNumber;
this.balance = initialBalance;
}

public int getAccountNumber() {


return accountNumber;
}

public int getBalance() {


return balance;
}

public void deposit(int amount) {


lock.lock();
try {
balance += amount;
System.out.println("Deposited $" + amount + " to account " + accountNumber
} finally {
lock.unlock();
}
}

public void withdraw(int amount) {


lock.lock();
try {
if (amount <= balance) {
balance -= amount;
System.out.println("Withdrawn $" + amount + " from account "
} else {
System.out.println("Insufficient funds for withdrawal from account "
}
} finally {
lock.unlock();
}
}
}

class BankTransaction implements Runnable {


private final BankAccount account;
private final int transactionAmount;
public BankTransaction(BankAccount account, int transactionAmount) {
this.account = account;
this.transactionAmount = transactionAmount;
}

@Override
public void run() {
// Simulate a bank transaction (deposit or withdrawal)
if (transactionAmount >= 0) {
account.deposit(transactionAmount);
} else {
account.withdraw(Math.abs(transactionAmount));
}
}
}

public class BankSimulation {


public static void main(String[] args) {
BankAccount account1 = new BankAccount(101, 1000);
BankAccount account2 = new BankAccount(102, 1500);

// Simulate concurrent bank transactions using threads


Thread thread1 = new Thread(new BankTransaction(account1, 200));
Thread thread2 = new Thread(new BankTransaction(account1, -300));
Thread thread3 = new Thread(new BankTransaction(account2, 500));

thread1.start();
thread2.start();
thread3.start();

try {
thread1.join();
thread2.join();
thread3.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

// Display final account balances


System.out.println("Final balance for account " + account1.getAccountNumber
System.out.println("Final balance for account " + account2.getAccountNumber
}
}

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

Synchronisation using Semaphores


Producer Consumer Problem : A T-Shirt Store Example
The Producer-Consumer problem is a classic synchronization problem where two
processes, the producer and the consumer, share a common, fixed-size buffer or store.
The producer produces items and adds them to the buffer, while the consumer consumes
items from the buffer. Semaphores are synchronization primitives that can be used to solve
this problem efficiently.
Problem Description Let’s use a T-shirt store as an analogy for the Producer-Consumer
problem. The T-shirt store has a limited capacity to store T-shirts. Producers can create T-
shirts and add them to the store, and consumers can buy T-shirts from the store. The
challenge is to ensure that the store doesn’t overflow with T-shirts or run out of stock.

Java Implementation-1 Using Semaphores (simplified implementation than what is


covered in class, here we don’t maintain an actual queue for T-Shirts, just the count)
import java.util.concurrent.Semaphore;

public class TShirtStore {


private static final int STORE_CAPACITY = 5;
private static Semaphore mutex = new Semaphore(1); // Controls access to critical sections
private static Semaphore empty = new Semaphore(STORE_CAPACITY); // Represents empty slots in the store
private static Semaphore full = new Semaphore(0); // Represents filled slots in the store
private static int tShirtCount = 0;

static class Producer implements Runnable {


@Override
public void run() {
try {
while (true) {
empty.acquire(); // Wait for an empty slot
mutex.acquire(); // Enter critical section

// Produce a T-shirt
System.out.println("Producer produces a T-shirt. Total T-shirts: "

mutex.release(); // Exit critical section


full.release(); // Signal that a T-shirt is ready to be consumed
Thread.sleep(1000); // Simulate production time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

static class Consumer implements Runnable {


@Override
public void run() {
try {
while (true) {
full.acquire(); // Wait for a T-shirt to be available
mutex.acquire(); // Enter critical section

// Consume a T-shirt
System.out.println("Consumer buys a T-shirt. Total T-shirts: "

mutex.release(); // Exit critical section


empty.release(); // Signal that a slot is available for production
Thread.sleep(1500); // Simulate consumption time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {


Thread producerThread = new Thread(new Producer());
Thread consumerThread = new Thread(new Consumer());

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;

Producer(Queue<Object> queue, int maxSize, String name, Semaphore ps,


this.queue = queue;
this.maxSize = maxSize;
this.name = name;
this.producerSemaphore = ps;
this.consumerSemaphore = cs;
}
@Override
public void run() {
while(true){
try {
producerSemaphore.acquire();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(queue.size()<this.maxSize){
System.out.println(this.name + " adding to queue, Size "
queue.add(new Object());
}
consumerSemaphore.release();
}
}
}

Consumer.java
package Multithreading.ProducerConsumer;

import java.util.Queue;
import java.util.concurrent.Semaphore;

public class Consumer implements Runnable{


private Queue<Object> queue;
private int maxSize;
private String name;
private Semaphore producerSemaphore;
private Semaphore consumerSemaphore;

Consumer(Queue<Object> queue, int maxSize, String name, Semaphore ps,


this.queue = queue;
this.maxSize = maxSize;
this.name = name;
this.producerSemaphore = ps;
this.consumerSemaphore = cs;
}

@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);

Producer p1 = new Producer(objects,6,"p1",producerSemaphore,consumerSemaphore


Producer p2 = new Producer(objects,6,"p2",producerSemaphore,consumerSemaphore
Producer p3 = new Producer(objects,6,"p3",producerSemaphore,consumerSemaphore

Consumer c1 = new Consumer(objects,6,"c1",producerSemaphore,consumerSemaphore


Consumer c2 = new Consumer(objects,6,"c2",producerSemaphore,consumerSemaphore
Consumer c3 = new Consumer(objects,6,"c3",producerSemaphore,consumerSemaphore
Consumer c4 = new Consumer(objects,6,"c4",producerSemaphore,consumerSemaphore
Consumer c5 = new Consumer(objects,6,"c5",producerSemaphore,consumerSemaphore

Thread t1 = new Thread(p1);


Thread t2 = new Thread(p2);
Thread t3 = new Thread(p3);
Thread t4 = new Thread(c1);
Thread t5 = new Thread(c2);
Thread t6 = new Thread(c3);
Thread t7 = new Thread(c4);
Thread t8 = new Thread(c5);

t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
}
}

Java Implementation -3 using Concurrent Data Structure Here is another


implementation in which you can use a ConcurrentLinkedQueue in place of semaphores to
ensure concurrency is handled well. Producer.java
import java.util.Queue;

public class Producer implements Runnable{


private Queue<Object> queue;
int maxSize;
String name;

public Producer(Queue<Object> queue,int maxSize, String name){


this.queue = queue;
this.maxSize = maxSize;
this.name = name;
}

@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;

public class Consumer implements Runnable{


private Queue<Object> queue;
int maxSize;
String name;

public Consumer(Queue<Object> queue,int maxSize, String name){


this.queue = queue;
this.maxSize = maxSize;
this.name = name;
}

@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;

Producer p1 = new Producer(q,maxSize,"p1");


Producer p2= new Producer(q,maxSize,"p2");
Producer p3 = new Producer(q,maxSize,"p3");

Consumer c1 = new Consumer(q,maxSize,"c1");


Consumer c2 = new Consumer(q,maxSize,"c2");
Consumer c3 = new Consumer(q,maxSize,"c3");
Consumer c4 = new Consumer(q,maxSize,"c4");
Consumer c5 = new Consumer(q,maxSize,"c5");

Thread t1 = new Thread(p1);


Thread t2 = new Thread(p2);
Thread t3 = new Thread(p3);
Thread t4 = new Thread(c1);
Thread t5 = new Thread(c2);
Thread t6 = new Thread(c3);
Thread t7 = new Thread(c4);
Thread t8 = new Thread(c5);

t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
t7.start();
t8.start();
}
}

Time To Try - Print In Order (LeetCode)


Try to solve the following problem using Semaphores Concept. - Print In Order - LeetCode
Solution
class Foo {
Semaphore semaSecond = new Semaphore(0);
Semaphore semaThird = new Semaphore(0);
public Foo() {

}
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.

Conditions for a deadlock


Mutual exclusion - The resource is held by only one process at a time and cannot be
acquired by another process.
Hold and wait - A process is holding a resource and waiting for another resource to
be released by another a process.
No preemption - The resource can only be released once the execution of the process
is complete.
Circular wait - A set of processes are waiting for each other circularly. Process P1
is waiting for process P2 and process P2 is waiting for process P1.

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.

1. Prevention and avoidance

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.

2. Detecting and recovering from deadlocks

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.

4. Tackling deadlocks at an application level

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:

The notify() method is called on an object within a synchronized context.


It wakes up one of the threads that are currently waiting on that object. Purpose:
notify() is used to signal that a condition (for which threads are waiting) has been met
and that one of the waiting threads can proceed.
It is essential to note that notify() only wakes up one waiting thread. If there are
multiple waiting threads, it is not determined which one will be awakened. Example:
synchronized (sharedObject) {
// Perform some operations and change the condition
conditionMet = true;

// Notify one of the waiting threads


sharedObject.notify();
}

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.

Producer Consumer using Wait() and Notify() Recording Link


Implementation -1 (Simplified) Here is a simplifed version of Producer Consumer as
discussed in above LIVE Class. ProducerConsumer.java
public class ProducerConsumer {

public void produce() throws InterruptedException {


synchronized (this){
System.out.println("Produced - T-shirt");
//release the lock on the shared resource and wait till some other invokes this
wait();
System.out.println("Going to produce another T-Shirt");
}
}

public void consume() throws InterruptedException {


Thread.sleep(1000);
Scanner sc = new Scanner(System.in);

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();

Thread t1 = new Thread(()->{


try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread t2 = new Thread(()->{
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

t1.start();
t2.start();
}
}

Implementation-2 A more robust Implementation is as follows:


import java.util.LinkedList;

class SharedBuffer {
private final LinkedList<Integer> buffer = new LinkedList<>();
private final int capacity;

public SharedBuffer(int capacity) {


this.capacity = capacity;
}

public void produce() throws InterruptedException {


synchronized (this) {
while (buffer.size() == capacity) {
// Buffer is full, wait for consumer to consume
wait();
}
// Produce an item and add to the buffer
int newItem = (int) (Math.random() * 100);
buffer.add(newItem);
System.out.println("Produced: " + newItem);

// Notify the consumer that an item is available


notify();
}
}

public void consume() throws InterruptedException {


synchronized (this) {
while (buffer.isEmpty()) {
// Buffer is empty, wait for producer to produce
wait();
}

// Consume an item from the buffer


int consumedItem = buffer.removeFirst();
System.out.println("Consumed: " + consumedItem);

// Notify the producer that a slot is available in the buffer


notify();
}
}
}

class Producer implements Runnable {


private final SharedBuffer sharedBuffer;

public Producer(SharedBuffer sharedBuffer) {


this.sharedBuffer = sharedBuffer;
}

@Override
public void run() {
try {
while (true) {
sharedBuffer.produce();
Thread.sleep(1000); // Simulate production time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

class Consumer implements Runnable {


private final SharedBuffer sharedBuffer;

public Consumer(SharedBuffer sharedBuffer) {


this.sharedBuffer = sharedBuffer;
}

@Override
public void run() {
try {
while (true) {
sharedBuffer.consume();
Thread.sleep(1500); // Simulate consumption time
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class ProducerConsumerExample {


public static void main(String[] args) {
SharedBuffer sharedBuffer = new SharedBuffer(5);
SharedBuffer sharedBuffer = new SharedBuffer(5);

Thread producerThread = new Thread(new Producer(sharedBuffer));


Thread consumerThread = new Thread(new Consumer(sharedBuffer));

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

// Simulate time passing


private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Switch the traffic lights for Road A and Road B


private void switchLights() {
System.out.println("Switching lights...");

try {
roadASemaphore.acquire(); // Acquire the semaphore for Road A
roadBSemaphore.release(); // Release the semaphore for Road B
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Simulate traffic on Road A


private void trafficOnRoadA() {
while (true) {
System.out.println("Road A: Green");
sleep(5); // Green light duration

System.out.println("Road A: Yellow");
sleep(2); // Yellow light duration

System.out.println("Road A: Red");
switchLights(); // Switch to Road B
}
}

// Simulate traffic on Road B


private void trafficOnRoadB() {
while (true) {
try {
roadBSemaphore.acquire(); // Acquire the semaphore for Road B
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

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
}
}

public static void main(String[] args) {


TrafficIntersectionControl control = new TrafficIntersectionControl

// Create and start threads for traffic on Road A and Road B


Thread roadAThread = new Thread(control::trafficOnRoadA);
Thread roadBThread = new Thread(control::trafficOnRoadB);

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;

private Semaphore availableBooks = new Semaphore(MAX_BOOKS, true);


private Semaphore studentSlots = new Semaphore(MAX_STUDENTS, true);

// Simulate time passing


private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Borrow a book from the library


private void borrowBook(String book, int studentId) {
try {
availableBooks.acquire(); // Acquire a book
System.out.println("Student " + studentId + " borrows " + book
sleep(2); // Simulate reading time
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

// Return a book to the library


private void returnBook(String book, int studentId) {
System.out.println("Student " + studentId + " returns " + book);
availableBooks.release(); // Release the returned book
}

// Simulate a student using the library


private void student(int studentId) {
while (true) {
try {
studentSlots.acquire(); // Acquire a student slot
String bookToBorrow = "Book " + (studentId % MAX_BOOKS + 1
borrowBook(bookToBorrow, studentId);
returnBook(bookToBorrow, studentId);
studentSlots.release(); // Release the student slot
sleep(1); // Wait before the next operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

public static void main(String[] args) {


Library library = new Library();

// 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();
}
}
}

LeetCode Multithreading Problems (HomeWork)


Dining Philoshpers - LeetCode
Fizz Buzz Multithreaded - LeetCode
Building H20 - LeetCode
Traffic Light Control

Additonal Resources
Udemy - Concurrency, Multithreading and Parallel Computing in Java
Memory Management in Operating Systems - Thrasing, Paging etc Interview Topics
– End –

You might also like