Understanding Multithreading Concepts
Understanding Multithreading Concepts
• Correct: If you have multiple CPU cores, they can execute multiple threads in
parallel, boosting performance.
• Clarification: The real performance boost depends on how well your application
is designed to leverage parallelism. Some tasks cannot be parallelized easily (e.g.,
tasks that depend on shared resources or have sequential dependencies).
1. OS Loading Applications:
• Correct: The operating system loads an application’s code and data into memory,
creating a process, which is an independent instance of the program. Each process
has its own address space and is isolated from others.
• Clarification: This isolation ensures that one process cannot directly access or
interfere with another process’s memory or resources.
2. Process vs. Thread:
• A process is the context in which the application runs, with its own memory
space.
When pc starts, OS is being loaded from disk to memory. With help of OS, we can
interact with hardware and CPU.
When we run any application OS takes application from disk and create its
instance on the memory. This instance called process or context of the application.
Each process completely isolated from other process that run on the system.
Memory Model
1. Stack:
• Correct: The stack stores local variables and function call parameters. Each
thread gets its own stack to keep track of its execution state independently of other
threads.
stack - region on the memory local variables are stored and passed into function.
instruction pointer - address of the next instruction to execute
2. Instruction Pointer:
• Correct: The instruction pointer (or program counter) keeps track of the next
instruction to execute. Each thread has its own instruction pointer to manage its
execution flow.
• Metadata: Information about the process (e.g., process ID, loaded libraries, etc.)
is shared.
Context Switching
1. What is Context Switching?
• Correct: Context switching is the process of stopping one thread (or process),
saving its current state (registers, program counter, etc.), and restoring the state of
another thread (or process) to begin/resume execution.
1. Stopping thread 1 (saving its state, like registers and program counter).
• Correct: Context switching is not “cheap.” It consumes CPU cycles to save and
restore thread/process states.
• Key Points:
• Each thread consumes memory and CPU resources for its state.
• Switching between threads in the same process is cheaper because they share
memory (code, heap, etc.).
3. Thread Thrashing:
• Solution: Limit the number of threads to align with the CPU core count and
ensure tasks are appropriately managed (e.g., using thread pools or task
schedulers).
Thread Scheduling
1. Dynamic Priority:
• Correct: Dynamic priority = static priority + bonus. This is how modern OSes
determine which thread gets CPU time.
• Static Priority: Set by developers or the system to define the base importance of
a thread.
• CPU usage (threads using less CPU might get higher bonuses).
2. Epochs:
• Correct: The OS divides time into epochs, where each thread gets a time slice to
execute its tasks.
• Point to Add: If a thread doesn’t finish its work in one epoch, it may continue in
the next based on its priority. Threads with higher priorities are scheduled first.
3. Example:
• How Threads Are Scheduled: The OS ensures time slices are allocated
dynamically, balancing responsiveness (UI) and background tasks (save or play).
Multithreaded vs. Multiprocessed Approaches
1. Multithreaded Approach:
• Example: A web server where threads handle requests but share memory for
caching.
2. Multiprocessed Approach:
Creating Thread:
Thread must implement Runnable interface.
1. Thread Scheduling:
• The JVM relies on the operating system’s thread scheduler to determine the order
of thread execution.
• Thread scheduling is not deterministic, meaning you cannot predict the exact
order in which threads will execute. This is why thread2 (lower priority) runs
before thread (higher priority).
2. Thread Priorities:
• Thread priorities are hints to the thread scheduler about which threads are more
important. However, they are not strict rules.
• A thread with Thread.MAX_PRIORITY (10) might not run before a thread with
Thread.MIN_PRIORITY (1) if the scheduler decides otherwise.
4. [Link]():
• The [Link](1500) in the main thread instructs it to pause for 1.5 seconds,
but this does not affect the execution of thread or thread2.
• While the main thread is sleeping, the other threads continue to execute.
• Default behavior:
• This behavior is specific to individual threads. The exception does not bring
down the entire application unless it’s the main thread.
• The run() method is overridden to specify what the thread will execute when it
starts.
4. Key Points:
• The start() method is used to start the thread. If you call run() directly instead of
start(), the code inside run() will execute in the main thread, not in a new thread.
In Java, the Thread class has two important methods related to execution: start()
and run(). They serve different purposes:
✅start()
Method
⚠️run()
Method
🔍 Summary Table
Feature start() run()
Starts a new thread ✅ Yes ❌ No
Calls run() method ✅ Internally ✅ Directly
Executes concurrently ✅ Yes ❌ No
Thread created? ✅ Yes ❌ No
✅ Yes — if run() throws an exception, it can cause the calling thread (like the
main thread) to fail.
→ The main thread crashes because the exception was thrown on the main thread,
not a separate one.
Now, the exception is thrown in the child thread, and the main thread keeps
running unless the exception is not handled and causes a crash in that thread.
✅ Best Practice:
Thread Termination
1. Resource Consumption of Threads:
• Correct: Even idle threads consume resources such as memory, kernel resources,
and CPU cache space.
• Clarification:
• Each thread has its own stack and thread control block (TCB) in memory.
• Too many idle threads may result in unnecessary resource usage, leading to
inefficient system performance.
• Detailed Reasons:
1. Thread Completes Work: A thread completes its task but remains alive,
consuming resources unnecessarily. Terminating it frees up these resources.
• [Link]()
• Using a shared variable like a boolean stopFlag that the thread periodically
checks.
[Link]()
1. What is [Link]()?
• This method is used to signal a thread that it should stop or adjust its behavior. It
does not forcibly stop the thread.
• The thread must explicitly check for the interrupt signal and handle it
appropriately.
2. How it works:
• If the thread is in a blocking method (e.g., sleep, wait, or join), the method will
throw an InterruptedException, which can be caught and handled.
• If the thread is not in a blocking method, the thread’s interrupted flag is set,
which the thread can check using [Link]() or isInterrupted().
3. Usage Example:
Daemon Threads
Daemon Threads:
background Thread that does not prevent the application from exiting if the
main thread terminates
background tasks do not black our application from terminates
example: file saving thread in a Text Editor
code in a worker thread is not under our control, and we do not want it to block our
application from terminating.
worker thread that uses an external librarys
[Link](true);
Because main thread ended make entire application terminates.
1. Definition:
• Correct: Daemon threads are background threads that do not prevent the JVM
(or application) from shutting down. Once all non-daemon threads (user threads)
are finished, the application will exit, even if daemon threads are still running.
• Examples: Tasks like logging, garbage collection, file saving, or any background
monitoring operations.
2. Key Characteristics:
3. Usage Example:
🔧 Why your interrupt() call doesn’t stop the pow() method:
Because interruption in Java is cooperative — not forced. Here’s the core idea:
Interrupting a thread only sets a flag — it does not stop the thread or break
loops unless the thread checks the flag itself.
The loop keeps running, and the thread never checks whether it was interrupted,
so it just goes on and on.
🧠 Summary:
Action Effect
[Link]() Sets the thread’s interrupted flag
Blocking method (e.g.
Throws InterruptedException and clears the flag
sleep)
Does nothing unless you manually check
CPU-bound loop
isInterrupted()
This is the example with blocking method which we do not need to check for
isInterrupted:
Key Concepts of Thread Coordination
1. Independence of Threads:
• Both threads run concurrently or in parallel (if there are multiple CPU cores).
3. Solution: [Link]()
• The [Link]() method allows one thread to wait for another thread to
complete its execution.
• Once the thread finishes its work, the waiting thread can proceed.
• This eliminates the need for busy waiting and ensures proper coordination.
1. Using [Link]():
• Instead of checking isFinished() in a loop, the join() method ensures that the main
thread waits for the worker thread to complete before printing the result.
2. Using isFinished:
• Since join() ensures that the main thread doesn’t proceed until the worker thread
has completed, we checking if calculation is finished, otherwise interrupting thread
and stopping it.
3. Handling InterruptedException:
• After the timeout, the main thread resumes execution, even if the worker thread
hasn’t finished.
• Example use case: If you’re calculating factorials for very large numbers and
don’t want to wait forever for one thread to finish.
Scenario:
Race Condition:
• If the main thread starts printing results before Thread A completes, you might
get incorrect or incomplete output.
Solution:
• Use [Link]() to ensure the main thread waits for Thread A to complete
before accessing its results.
Race condition
A race condition is a type of bug that occurs in concurrent programming when
two or more threads access shared data at the same time, and the final result
depends on the order in which the threads execute.
🧠 In simple terms:
A race condition happens when the program behaves differently each time you
run it, depending on which thread "wins the race" to access or change shared
data.
❗ Expected Output:
2000
⚠️Actual Output:
Multiple threads doing this at the same time can interfere with each other.
3. Latency formula:
{Latency} = {T}/{N}
• Reality: Other processes consume CPU and memory, so the actual ideal N is
lower than the total number of cores.
• If the task involves I/O or waiting, then N can exceed the number of cores.
3. Cost of Parallelization & Aggregation
• Too many threads can lead to thread thrashing (excessive context switching).
2. Synchronization Overhead:
• Solution: Minimize shared state and use thread-local storage where possible.
• If subtasks are unevenly distributed, some threads may finish earlier than others.
• Solution: Use a work-stealing approach, where idle threads take tasks from busy
ones.
4. Aggregation Cost:
• Example:
• Example:
• Recursive function calls where the next step depends on the previous one.
• Some parts can run in parallel, but others must run sequentially.
• Example:
• Sorting Algorithms (Merge Sort, Quick Sort)
• Database Queries
• Optimal Strategy:
• Amdahl’s Law:
• N = number of processors.
Final Thoughts
The format used in our Image Processing example is a version of the RGB family
called ARGB, where A stands for alpha (transparency)
Since we have 4 bytes, we can store the entire color of a pixel in a variable of type
int.
In the Image Processing example we have the following methods that extract
individual components of a pixel:
Let's explain each method, in particular the math that happens to get each color
component.
In order to get a particular component (red, green, or blue), we need to first get rid
of all the other color components in the pixel, while keeping the desired
component.
A bitmask defines which bits we want to keep, and which bits we want to
clear.
We apply a bitwise AND with 0x00 (0000 0000 in binary) to get rid of a
component since X AND 0 = 0, for any X.
We apply a bitwise AND with 0xFF (1111 1111 in binary) to keep the value of a
component since X AND 1 = X, for any X.
However, after applying a bitmask we are not done. We still need to shift the byte
representing our component to the lowest byte.
For example in the getRed(..) method, after we apply the bitmask on 0x76543210
we end up with 0x00540000, but what we need is 0x00000054
So we need to shift all the bits in the result of the bitmask to the right., using the
>> operator.
For the blue color extraction, we don't need to perform any shifting since it's
already the right-most byte.
For the green color extraction, we need to move all the bits 1 byte (8 bits) to
the right.
For the red color extraction, we need to move all the bits 2 bytes (16 bits) to
the right.
When building a pixel's color from individual red, green and blue components we
had the following method:
In the above code, we perform the opposite of color component extraction. We
take each component and shift it to the right place in the ARGB pixel
representation.
Blue is placed at the lowest byte so we simply bitwise OR the pixel color
representation with the blue component
Green needs to be placed at the second byte so it is first shifted 1 byte (8
bits) to the left, and then is bitwise ORed with the pixel color
Similarly, red needs to be placed at the third byte so its component is shifted
2 bytes (16 bits) to the left, and then it is bitwise ORed with the pixel color
The final step is to set the transparency level to the highest, making the color
completely opaque (0 levels mean fully transparent, 255 means fully opaque).
That is achieved by setting the left-most byte, representing the alpha component to
0xFF which is 1111 1111 in binary.
Throughput
Throughput – The Number of tasks completed in a given period
In this case max theoretical throughput is N/T. But in practice this is much more
likely to achieve.
Reason is that tasks are inherently unrelated and independent from each other.
Thread Pooling
What: Reusing a pool of worker threads instead of creating/destroying
threads per task.
Why: Creating threads is expensive (memory and time).
How it improves throughput:
o Reduces latency per task (no thread creation delay).
o Keeps CPUs busy with new tasks.
o Ensures a controlled number of threads (avoids resource exhaustion).
Creating threads once and reusing them for feature tasks instead of recreating
threads.
✅ Summary
Concept Purpose Notes
High throughput = better system
Throughput Maximize tasks/time
capacity
Task Enables finer-grained Only helps if subtasks can run
Decomposition parallelism concurrently
Run independent tasks at Most impactful if tasks don’t
Parallel Execution
once block each other
Reduce thread management Saves time, memory, and boosts
Thread Pooling
overhead throughput
Question Throughput
We are running an HTTP server on a single machine. Handling of the HTTP
requests is delegated to a fixed-size pool of threads. Each request is handled by a
single thread from the pool by performing a blocking call to an external database
which may take a variable duration, depending on many factors. After the response
comes from the database, the server thread sends an HTTP response to the user.
Assuming we have a 64 core machine. What would be the optimal thread pool size
to serve the HTTP request?
Correct Answer is: more than 64. There is no way to know it.
That's correct! Since the threads are not in the "running" state, all the time while
serving the incoming requests, we may have all of the threads blocked on IO
(waiting for a response from the database), but the CPU is not actually executing
any tasks). So if we create more threads to handle the incoming requests, we will
get better throughput. There is no way of knowing the best number of threads
ahead of time since more threads means more requests can be handled, but also
more overhead and context switching. So we need to perform a load test.
🧠 Scenario Summary
❌ Not necessarily!
Because the threads are doing blocking I/O, most of their time is spent waiting,
not using the CPU.
So in this case:
If you only have 64 threads, and all are blocked waiting for DB responses →
the server can’t accept new requests.
If you have 200–300 threads, then:
o Some threads are waiting.
o Others are ready to handle incoming requests.
o So your throughput increases.
More threads = more memory usage and context switching (CPU time wasted
switching between threads). So:
So what do we do?
➡️Answer: Load testing.
Throughput (requests/sec)
Latency (how long requests take)
CPU & memory usage
So if:
✅ Summary
You wrote:
✅ That’s correct.
This looks like one operation, but it's actually 3 steps behind the scenes:
So this is not atomic. Another thread can interrupt between these steps.
items starts at 0
10,000 increments → +10,000
10,000 decrements → -10,000
Final result should be 0
But you'll often get a wrong result like -123, 57, or anything else.
😱 Why the wrong result?
Because items++ and items-- are not atomic, two threads can interfere with each
other.
Let’s say:
items = 5
Thread A (incrementing):
Reads items = 5
Adds 1 → 6
Before it writes, Thread B runs
Thread B (decrementing):
Reads items = 5
Subtracts 1 → 4
Writes items = 4
Writes items = 6
Imagine two threads calling increment() and decrement() at the same time on the
same object. Without synchronization, both could read and modify the items
variable at the same time, causing race conditions and inconsistent results.
2. Synchronized blocks
Feature Description
synchronized method Locks on this (whole method is locked)
synchronized block You choose what object to lock
Locking purpose Prevents race conditions (thread safety)
Pitfall Mixing locks (this vs custom lock)
Volatile
For better performance we should synchronize as little as possible
Most operations are non-atomic.
1. Int
2. Short
3. Byte
4. Float
5. Char
6. Boolean
Long and double are exceptions because they are 64-bit longs. Java cannot
guarantee even if you have 64-bit computer.
If we declare volatile double and long variable read from and write to them are
atomic and thread-safe, on the other words they are guaranteed to performed by
single hardware operations.
When one thread writes to a volatile variable, other threads will see the
updated value immediately.
🧪 Example
If another thread sets running = false, this thread will see the change
immediately, even if the value was cached.
Normally, long and double are 64-bit, and may be updated in two 32-bit
halves on 32-bit JVMs.
Without volatile, another thread could see a partially updated value (a torn
read).
Marking them volatile ensures they are read/written atomically as 64-bit
values.
✅ Summary Table
Feature volatile guarantees
Visibility across threads ✅ Yes
Prevent caching by threads ✅ Yes
Feature volatile guarantees
Prevent instruction reordering ✅ Yes
Atomicity of compound operations ❌ No
Atomicity of long/double ✅ Only if volatile
You have a single variable that is read and written by multiple threads
Operations are simple reads/writes, not compound actions
Example:
Here, volatile ensures that changes to running are visible immediately.
Even if counter is volatile, two threads might read 0 and both write 1.
This ensures:
Example
Your understanding is close but needs some clarification. The volatile keyword in
Java guarantees atomicity of read and write operations on variables of certain
data types like long and double.
In the case of counter++, which is a compound operation, volatile does not protect
against race conditions, because multiple threads could read and update the value
simultaneously, leading to lost updates.
Summary:
Reads and writes to primitive types that are 32 bits or smaller (int, float,
char, short, byte, and boolean) are guaranteed to be atomic — but only in
terms of individual operations.
For long and double, which are 64-bit, reads/writes might not be atomic
unless declared volatile.
Visibility: When one thread modifies a volatile variable, other threads will
see the updated value immediately.
Ordering: volatile adds happens-before relationships — the compiler and
CPU will not reorder instructions in a way that breaks visibility.
Why should I declare integer variables volatile? Aren't they default atomic?
⚠️Example
Even though i is int, the thread may never terminate! Why? Because without
volatile, the update to i might not be visible to the other thread. Declaring i as
volatile would fix that.
🔚 Summary
1. Atomicity
Definition: An operation is atomic if it happens completely or not at all, with no chance
for another thread to observe it half-done.
Example with int: On the JVM, reads/writes of 32-bit int values are atomic.
That means if Thread A writes x = 42 and Thread B reads x, B will either see the old
value or 42. It will never see a “torn” value (like half-updated bits).
What it doesn’t guarantee: Atomicity doesn’t ensure when other threads will see the
update. That’s where visibility comes in.
2. Visibility
Definition: Visibility means that when one thread updates a variable, other threads will
actually see the new value, not a cached/stale one.
Why it’s an issue:
o Each thread may keep its own copy of variables in CPU registers or caches.
o Without special rules, one thread’s changes might not be written back to main
memory immediately.
o So another thread could keep reading the old value forever, even though another
thread updated it.
Visibility guarantee: Every read of a volatile variable reads directly from main memory.
Every write to a volatile variable is immediately written to main memory.
Happens-before relationship: A write to a volatile happens-before any subsequent read
of that variable.
⚠️But compound actions (like count++) are not atomic, even if count is volatile. For
those, you need synchronized or AtomicInteger.
4. Putting It Together
An int write/read is atomic (no half-values), but not necessarily visible across threads.
volatile int ensures atomic + visible reads/writes, but still not atomic for composite
operations.
Example:
Because flag isn’t volatile, Thread B might never see the update.
With volatile:
✅ Summary:
Atomicity = indivisible update (int read/write is atomic).
Visibility = threads see each other’s changes (volatile provides this).
Volatile ensures visibility and atomic reads/writes, but not atomic compound operations.
The Java programming language provides a second mechanism, volatile fields, that
is more convenient than locking for some purposes.
A field may be declared volatile, in which case the Java Memory Model ensures
that all threads see a consistent value for the variable (§17.4).
If, in the following example, one thread repeatedly calls the method one (but no
more than Integer.MAX_VALUE times in all), and another thread repeatedly calls
the method two:
class Test {
static int i = 0, j = 0;
then method two could occasionally print a value for j that is greater than the value
of i, because the example includes no synchronization and, under the rules
explained in §17.4, the shared values of i and j might be updated out of order.
One way to prevent this out-or-order behavior would be to declare methods one
and two to be synchronized (§[Link]):
class Test {
static int i = 0, j = 0;
This prevents method one and method two from being executed concurrently, and
furthermore guarantees that the shared values of i and j are both updated before
method one returns. Therefore method two never observes a value for j greater
than that for i; indeed, it always observes the same value for i and j.
class Test {
This allows method one and method two to be executed concurrently, but
guarantees that accesses to the shared values for i and j occur exactly as many
times, and in exactly the same order, as they appear to occur during execution of
the program text by each thread. Therefore, the shared value for j is never greater
than that for i, because each update to i must be reflected in the shared value for i
before the update to j occurs. It is possible, however, that any given invocation of
method two might observe a value for j that is much greater than the value
observed for i, because method one might be executed many times between the
moment when method two fetches the value of i and the moment when method two
fetches the value of j.
This means:
In a single thread, each action happens-before those that come later in the code.
Writes to a final field in a constructor happen-before any other thread reads that
object after the constructor finishes — if the object reference doesn’t escape
during construction.
🧠 Summary
Concept Guarantees
Happens-before Visibility + Ordering
No happens-before No guarantees (may reorder, stale values)
Established via synchronized, volatile, [Link], [Link], etc.
Deadlock
🔁 Simple Definition:
Deadlock is like a circular wait — Thread A holds Resource 1 and waits for
Resource 2, while Thread B holds Resource 2 and waits for Resource 1. Neither
can proceed.
🧠 Real-Life Analogy:
There are 4 conditions that must be true simultaneously for a deadlock to occur:
Strategy Description
Lock Ordering Always acquire locks in a fixed global order.
Try-Lock with
Use tryLock(timeout) to avoid waiting forever.
Timeout
Avoid Nested
Don’t acquire multiple locks at once if not needed.
Locks
Deadlock
Some advanced systems can detect and recover from deadlocks.
Detection
Use tools like [Link] (ReentrantLock,
Using Higher-
ExecutorService, etc.) which help avoid deadlock-prone
Level Tools
patterns.
Reentrant Lock
🔐 What is ReentrantLock?
ReentrantLock is a class in Java that works like synchronized, but with more
control and flexibility.
🔁 "Reentrant" means:
A thread can acquire the same lock multiple times without getting stuck (it must
release it the same number of times).
✅ Basic Usage:
It's similar to:
It does not guarantee that the longest-waiting thread gets the lock next.
A thread may "cut in line" and acquire the lock before others.
🔁 Fair Mode:
When fair = true, the lock grants access to the thread that has been waiting
the longest (like a queue: FIFO).
Trade-off:
🚫 What is lockInterruptibly()?
Normally, lock() is not interruptible — if a thread is blocked waiting for the lock, it
cannot be interrupted.
But lockInterruptibly() allows a thread to respond to interruption:
🔥 Why is this useful?
Imagine your thread is waiting for a lock, but the app wants to cancel or shut
down. You can interrupt it.
⏳ What is tryLock()?
Without timeout:
With timeout:
🔚 Summary Table
lock() tryLock()
Blocks the thread until the lock is Does not block — either gets the lock or
acquired moves on
Useful when you can skip if the lock is
Useful when you must get the lock
unavailable
May cause UI freezing if used on UI
Keeps UI responsive
thread
🤖 Meanwhile:
PriceUpdater is a background thread that sleeps, then locks and updates data.
It might be holding the lock when AnimationTimer tries to access the same
data.
This conflict causes the UI thread to wait, degrading responsiveness.
With tryLock():
If the background thread holds the lock, the UI thread skips the update and
continues.
This keeps the UI smooth — the data just doesn't update for that frame,
which is fine.
The next frame (within ~16ms for 60 FPS), it will try again.
You sacrifice some data freshness for consistent responsiveness, which is the
right trade-off in UI applications.
🔄 Analogy
🧠 TL;DR
lock() blocks the UI thread when the PriceUpdater holds the lock → causes
lag.
tryLock() allows the UI to skip updating data if it's not available, keeping
the UI fast.
This is a classic UI vs. background thread synchronization problem —
and you're solving it correctly by using tryLock().
ReentrantReadWirteLock
🔐 What is ReentrantReadWriteLock?
ReentrantReadWriteLock is a Java lock that separates access into read and write
locks.
✅ When to use?
🧪 Example:
❓ How is this different from volatile?
volatile only ensures visibility and ordering — it does not prevent data
races for compound operations.
ReentrantReadWriteLock controls access, making sure that reads and writes
do not clash.
📌 Summary
Scenario Use...
Simple flag or state variable volatile
Multiple readers, rare writers ReentrantReadWriteLock
High contention on read/write ReentrantLock or other concurrency tools
✅ Final Advice:
🔑 What is a Semaphore?
✅ Basic Concept
🔑 What is a Semaphore?
In Java:
✅ Key Methods:
Method Description
acquire() Waits until a permit is available, then takes it
tryAcquire() Tries to take a permit, returns false if none
release() Returns a permit to the pool
🧵 Use Case: Producer-Consumer Problem
🧩 Ingredients:
Semaphore Purpose
empty Ensures producer waits if full
full Ensures consumer waits if empty
mutex Ensures exclusive access to buffer
✅ Summary
🆚 Semaphore vs Lock
Feature Semaphore Lock (e.g., ReentrantLock)
Allows multiple
✅ Yes (configurable count) ❌ No (only one thread at a time)
threads?
Control granularity Coarse (permits) Fine (exclusive access)
Can be used for ✅ Yes (e.g., Producer-
❌ Not directly
signaling Consumer)
❌ No (any thread can ✅ Yes (only locking thread can
Thread ownership
release) unlock)
Fairness option ✅ Yes (optional) ✅ Yes (optional)
🧠 Summary
Question 1:
In this question, we will design a Barrier class.
When running tasks by multiple threads concurrently, sometimes we would like to
coordinate the work to guarantee that some portion of the work is done by all
threads before the rest of the work is performed.
If we have 3 threads executing this task concurrently, we would like the output to
look like this:
The order of the execution of each part is not important. But we want to make sure
that all threads finish part1 before any thread can go ahead and perform part2
So:
We have:
Now, step-by-step:
🔒 Step 1: Thread reaches barrier
Each thread increments counter under a lock to track how many threads have
arrived.
If counter == numberOfWorkers, this is the last thread, and we allow all threads
to proceed.
The last thread does not need to acquire, it just lets the other threads go by
releasing enough permits.
🔧 Java API:
✅ Methods:
✅ Similarities:
❌ Limitations:
🔹 3. Inter-thread Communication
When an object is first created, its wait set is empty. Elementary actions that add
threads to and remove threads from wait sets are atomic. Wait sets are manipulated
solely through the methods [Link], [Link], and [Link].
Wait set manipulations can also be affected by the interruption status of a thread,
and by the Thread class's methods dealing with interruption. Additionally, the
Thread class's methods for sleeping and joining other threads have properties
derived from those of wait and notification actions.
17.2.1. Wait
Wait actions occur upon invocation of wait(), or the timed forms wait(long
millisecs) and wait(long millisecs, int nanosecs).
Let thread t be the thread executing the wait method on object m, and let n be the
number of lock actions by t on m that have not been matched by unlock actions.
One of the following actions occurs:
If n is zero (i.e., thread t does not already possess the lock for target m), then
an IllegalMonitorStateException is thrown.
If this is a timed wait and the nanosecs argument is not in the range of 0-
999999 or the millisecs argument is negative, then an
IllegalArgumentException is thrown.
If thread t is interrupted, then an InterruptedException is thrown and t's
interruption status is set to false.
Otherwise, the following sequence occurs:
1. Thread t is added to the wait set of object m, and performs n unlock
actions on m.
2. Thread t does not execute any further instructions until it has been
removed from m's wait set. The thread may be removed from the wait
set due to any one of the following actions, and will resume sometime
afterward:
o A notify action being performed on m in which t is selected for
removal from the wait set.
o A notifyAll action being performed on m.
o An interrupt action being performed on t.
o If this is a timed wait, an internal action removing t from m's
wait set that occurs after at least millisecs milliseconds plus
nanosecs nanoseconds elapse since the beginning of this wait
action.
o An internal action by the implementation. Implementations are
permitted, although not encouraged, to perform "spurious wake-
ups", that is, to remove threads from wait sets and thus enable
resumption without explicit instructions to do so.
Notice that this provision necessitates the Java coding practice of using wait only
within loops that terminate only when some logical condition that the thread is
waiting for holds.
3. Each thread must determine an order over the events that could cause
it to be removed from a wait set. That order does not have to be
consistent with other orderings, but the thread must behave as though
those events occurred in that order.
4. For example, if a thread t is in the wait set for m, and then both an
interrupt of t and a notification of m occur, there must be an order
over these events. If the interrupt is deemed to have occurred first,
then t will eventually return from wait by throwing
InterruptedException, and some other thread in the wait set for m (if
any exist at the time of the notification) must receive the notification.
If the notification is deemed to have occurred first, then t will
eventually return normally from wait with an interrupt still pending.
5. Thread t performs n lock actions on m.
6. If thread t was removed from m's wait set in step 2 due to an interrupt,
then t's interruption status is set to false and the wait method throws
InterruptedException.
🔍 First, What Is wait()?
"Pause here until you're notified, or until a timeout, or until you're interrupted."
But for a thread to wait(), it must already hold the lock on the object it's calling
wait() on — usually done using synchronized.
Let’s say:
🧩 Step 1:
🧩 Step 2:
Thread goes to sleep and waits to be woken up. It can wake up because of:
🧩 Step 3:
The thread must act like it received the wake-up events in some consistent order
(even though the order might not be obvious in practice).
For example:
🧩 Step 4:
⚠️It doesn't just start running immediately — it must wait again until it can
reacquire the lock (like synchronized does).
🧩 Step 5:
It resumes normally
Because of spurious wake-ups and race conditions, the general pattern is:
Never do:
✅ Summary Table
Concept Meaning
wait() Waits until notified, interrupted, or timeout
Must hold lock? ✅ Yes, or you'll get IllegalMonitorStateException
Releases lock? ✅ Yes, temporarily, while waiting
Reacquires lock? ✅ Yes, before continuing after wakeup
Spurious wakeups? ✅ Can happen, so always wait in a loop
Can throw exception? ✅ Yes: IllegalMonitorStateException, InterruptedException
17.2.2. Notification
Notification actions occur upon invocation of methods notify and notifyAll.
Let thread t be the thread executing either of these methods on object m, and let n
be the number of lock actions by t on m that have not been matched by unlock
actions. One of the following actions occurs:
This is the case where thread t does not already possess the lock for target m.
If n is greater than zero and this is a notify action, then if m's wait set is not
empty, a thread u that is a member of m's current wait set is selected and
removed from the wait set.
There is no guarantee about which thread in the wait set is selected. This removal
from the wait set enables u's resumption in a wait action. Notice, however, that u's
lock actions upon resumption cannot succeed until some time after t fully unlocks
the monitor for m.
If n is greater than zero and this is a notifyAll action, then all threads are
removed from m's wait set, and thus resume.
Notice, however, that only one of them at a time will lock the monitor required
during the resumption of wait.
You use notify() or notifyAll() to wake up threads that are currently paused with
wait() on the same object.
They don’t immediately start running — they just move from the wait set to the
ready-to-acquire-lock state.
💡 High-Level Analogy:
If the wait set of object m is not empty, pick one thread u from it randomly and
remove it from the wait set.
🔁 Sequence Example:
Initial:
What Happens?
💡 Important Notes
Concept Explanation
Must be synchronized You must hold the lock to call notify()/notifyAll()
notify() Wakes up one waiting thread
notifyAll() Wakes up all waiting threads
Resumption Threads don’t resume until they reacquire the lock
No fairness No guarantee which thread is picked for notify()
Let t be the thread invoking [Link], for some thread u, where t and u may be
the same. This action causes u's interruption status to be set to true.
Additionally, if there exists some object m whose wait set contains u, then u is
removed from m's wait set. This enables u to resume in a wait action, in which case
this wait will, after re-locking m's monitor, throw InterruptedException.
You're asking that thread to stop what it's doing, especially if it's waiting,
sleeping, or blocking. It does not forcibly kill the thread — instead:
"If u is in some object m's wait set, remove it from the wait set."
"After re-locking m's monitor, wait() throws InterruptedException."
Method Use
[Link]() Check if a thread has been interrupted (does not clear it)
[Link]() Check if current thread is interrupted, and clear the flag
✅ Output:
✅ Summary
return normally from wait, while still having a pending interrupt (in other
words, a call to [Link] would return true)
return from wait by throwing an InterruptedException
The thread may not reset its interrupt status and return normally from the call to
wait.
🧠 What happens if a thread is both interrupted and notified while it's waiting?
"The thread may not reset its interrupt status and return normally from wait."
In other words:
Imagine:
Now from another thread:
Let’s say:
🟰 Java ensures fairness: the notify() must be received by someone, not wasted
on the interrupted thread.
✅ Final Rule
"If a thread is both interrupted and woken via notify, and that thread returns from
wait by throwing an InterruptedException, then some other thread in the wait set
must be notified."
🔍 Summary Table
For example, in the following (broken) code fragment, assume that [Link] is a
non-volatile boolean field:
while (![Link])
[Link](1000);
The compiler is free to read the field [Link] just once, and reuse the cached
value in each execution of the loop. This would mean that the loop would never
terminate, even if another thread changed the value of [Link].
✅ [Link](milliseconds)
Makes the current thread pause execution for a set amount of time.
The thread keeps any locks it currently holds (doesn’t release monitors).
After sleeping, the thread can resume if the scheduler allows (i.e., when
CPU is available).
✅ [Link]()
Hints to the scheduler: “I’m willing to give up the CPU, let others run.”
It might do nothing — depends on the system scheduler.
The thread may resume immediately or after others get CPU time.
❗ BUT: No memory synchronization!
So, the compiler is allowed to keep using old/stale values stored in thread-local
registers or CPU cache.
Assume:
The current thread may cache [Link] as false and never reload it from
memory.
sleep() does not force the thread to reload [Link].
So, the thread might never see the update made by the other thread.
➡️The loop never ends — even though another thread did the update
correctly!
Use volatile:
Now the thread:
🔄 Summary
Concept Explanation
sleep() Pauses thread for a time without releasing locks
yield() Suggests giving up CPU time; may do nothing
❌ Memory
Neither ensures memory synchronization
visibility
🚫 Compiler Compiler may cache variables, unless volatile or synchronization
freedoms is used
Threads may run infinitely if relying on non-volatile flags and
❗ Real problem
assuming sleep() will fix visibility
Atomic operations
Package [Link]
These classes are not general purpose replacements for [Link] and
related classes. They do not define methods such as equals, hashCode and
compareTo. Because atomic variables are expected to be mutated, they are poor
choices for hash table keys.
In addition to classes representing single values and arrays, this package contains
Updater classes that can be used to obtain compareAndSet and related operations
on any selected volatile field of any selected class. These classes predate the
introduction of VarHandle, and are of more limited use.
AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, and
AtomicLongFieldUpdater are reflection-based utilities that provide access to the
associated field types. These are mainly of use in atomic data structures in which
several volatile fields of the same node (for example, the links of a tree node) are
independently subject to atomic updates. These classes enable greater flexibility in
how and when to use atomic updates, at the expense of more awkward reflection-
based setup, less convenient usage, and weaker guarantees.
AtomicInteger
AtomicInteger or synchronized?
what is the difference between this 2 code? results are same. What does atomic
integer do? does it use syncronized keyword in the background?
✅ Version 1: Using AtomicInteger
🔍 How it works:
🔍 How it works:
But:
This can't be done correctly with AtomicInteger using basic methods — you'd need
compareAndSet().
✅ TL;DR
QUESTION
Is this code thread-safe?
🚨 The Main Issue
The class is not thread-safe because the getAverage() method is not atomic with
respect to the overall operation of:
1. Reading sum
2. Reading count
3. Calculating average
4. Resetting both values
These four steps are not performed as a single atomic unit, and therefore:
addSample(...)
getAverage()
🔍 Real-World Problem Example
Let’s say:
💥 The value added by Thread B (sample = 10) is now lost, because it happened
between read and reset.
That’s why this class is not thread-safe as a whole, even if the individual
variables are atomic.
✅ Ways to Fix It
Option 2: Use LongAdder (faster under high concurrency, but no way to reset
safely with average)
✅ Fully thread-safe
✅ Lock-free
✅ Doesn't lose updates
✅ Summary
When a thread performs an I/O operation (e.g., read() or write()), it gets blocked
(paused) until that operation finishes.
Feature Behavior
Simplicity ✅ Easy to code and understand
Resource Usage ❌ Each connection/thread blocks until done
Scalability ❌ Not efficient with many users
Use Case Good for small-scale apps or scripts
🕸 In Server Context
✅ Summary
Creates a new thread for each task (up to 10,000 in your case) unless idle
threads are available
No limit on max threads (can cause OOM or CPU overload)
Each task runs [Link](1000) (1 second)
✅ Pros:
❌ Cons:
✅ Pros:
❌ Cons:
Only 1000 tasks can run in parallel; the rest wait in the queue
Total time will be roughly 10 batches × 1 second = ~10 seconds
Poor CPU usage if you have fewer than 1000 threads but many cores
✅ Pros:
❌ Cons:
🧠 Conceptual Differences
📝 Conclusion
Even though [Link](10) is the major time cost, the loop itself adds:
In contrast:
If this were real I/O (e.g., network or file), having 100 small I/O calls per task
would:
But since you're using [Link](), this effect is small unless the OS is very
busy.
✅ Summary
Reason Impact
Loop overhead Minor
[Link](10) accuracy Moderate
Thread scheduler / context switch overhead Moderate
GC or memory pressure Minor
Non-Blocking IO
Only callback responses will delay, but db request and response gonna work.
🔍 Blocking I/O vs Non-Blocking I/O in Java
✅ Blocking I/O
Thread waits (is blocked) until the I/O operation (e.g., read/write) finishes.
Simple to write but scales poorly: one thread per client.
✅ Non-Blocking I/O
Thread does not block; it registers interest in events (e.g., data available).
Uses callbacks, selectors, or futures.
Allows handling many connections with few threads.
🔚 Summary
Blocking I/O: each request needs a thread — easy but inefficient for scale.
Non-blocking I/O: few threads serve many clients — complex but efficient.
Use [Link] or frameworks like Netty, Vert.x for high-performance non-
blocking applications.
🔧 Threading Model: Thread-Per-Core
This suggests you're not creating a new thread per request, but rather have a
limited number of threads (likely equal to number of CPU cores — a common
choice).
Unlike traditional platform threads (which are tied to OS threads), virtual threads
use a continuation-based model, meaning:
When they block (e.g., on I/O), the JVM parks them and frees up the underlying
OS thread — very efficient!
Practice Description
Virtual threads excel in apps doing network or
✅ Use for I/O-heavy apps
file I/O (e.g., HTTP servers, DB calls).
Don’t do heavy CPU work inside virtual threads.
✅ Avoid blocking on CPU
Use CPU-bound thread pools if needed.
Most blocking I/O in Java (sockets, JDBC,
✅ Use standard Java APIs
HttpClient) are compatible.
Makes reasoning about concurrent flows easier
✅ Prefer structured concurrency
(try-with-resources on executors).
❌ Don’t manually manage large Let virtual threads handle scale — no need for
numbers of OS threads tuning complex thread pools.
❌ Avoid legacy APIs that block Example: old native libraries or thread-unsafe
OS threads code.
Each request gets its own virtual thread — simple and scalable.
2. Databases
Most JDBC drivers are still blocking. But using them inside virtual threads is fine:
🔄 Summary
Feature Traditional Threads Virtual Threads
OS Thread Yes No
Blocking Cost High Low
Max Threads Thousands Millions
Best For CPU + I/O apps High-concurrency I/O apps
Thread Pool Needed? Yes Often no