Assignment 9 – Theoretical Questions

This commit is contained in:
2026-06-25 11:55:19 +03:30
parent fa95f2ebeb
commit e699d71f20
11 changed files with 607 additions and 49 deletions
Vendored
BIN
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="HW-09-Advanced-Multithreading" />
</profile>
</annotationProcessing>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="25" project-jdk-type="JavaSDK" />
</project>
Generated
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
Binary file not shown.
+498
View File
@@ -0,0 +1,498 @@
\documentclass[12pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[margin=2.5cm]{geometry}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{hyperref}
\usepackage{parskip}
\usepackage{booktabs}
\usepackage{array}
\usepackage{titlesec}
\usepackage{fancyhdr}
\usepackage{amsmath}
\usepackage{enumitem}
% ── Page style ──────────────────────────────────────────────────────────────
\setlength{\headheight}{14.5pt}
\pagestyle{fancy}
\fancyhf{}
\rhead{Advanced Programming Assignment 9}
\lhead{Faraz Ardeh}
\cfoot{\thepage}
% ── Section formatting ───────────────────────────────────────────────────────
\titleformat{\section}{\large\bfseries}{Question \thesection.}{0.6em}{}
\titleformat{\subsection}{\normalsize\bfseries}{}{0em}{}
% ── Java code style ─────────────────────────────────────────────────────────
\definecolor{javakw}{rgb}{0.13,0.13,0.60}
\definecolor{javastr}{rgb}{0.63,0.13,0.13}
\definecolor{javacmt}{rgb}{0.38,0.38,0.38}
\definecolor{codebg}{rgb}{0.97,0.97,0.97}
\definecolor{codefr}{rgb}{0.82,0.82,0.82}
\lstdefinestyle{java}{
language=Java,
basicstyle=\ttfamily\small,
keywordstyle=\color{javakw}\bfseries,
stringstyle=\color{javastr},
commentstyle=\color{javacmt}\itshape,
numberstyle=\tiny\color{gray},
numbers=left,
stepnumber=1,
numbersep=8pt,
backgroundcolor=\color{codebg},
frame=single,
rulecolor=\color{codefr},
breaklines=true,
breakatwhitespace=false,
showstringspaces=false,
tabsize=4,
captionpos=b,
}
\lstset{style=java}
% ── Inline code ─────────────────────────────────────────────────────────────
\newcommand{\code}[1]{\texttt{\small #1}}
% ────────────────────────────────────────────────────────────────────────────
\begin{document}
% ── Title page ───────────────────────────────────────────────────────────────
\begin{titlepage}
\centering
\vspace*{3cm}
{\Huge\bfseries Advanced Multithreading\par}
\vspace{0.8cm}
{\large Assignment 9 Theoretical Questions\par}
\vspace{0.4cm}
{\large Advanced Programming (AP)\par}
\vspace{2cm}
\rule{0.6\linewidth}{0.5pt}\\[0.4cm]
{\large\bfseries Faraz Ardeh\par}
\vspace{0.3cm}
{\normalsize fa.ardeh@gmail.com\par}
\vspace{0.6cm}
{\normalsize Shahid Beheshti University\par}
\vspace{0.3cm}
{\normalsize June 2026\par}
\vfill
\end{titlepage}
\tableofcontents
\newpage
% ════════════════════════════════════════════════════════════════════════════
\section{What Are Atomic Variables?}
% ════════════════════════════════════════════════════════════════════════════
An \textbf{atomic variable} is a variable whose read-modify-write operations are
executed as a single, indivisible step -- no other thread can observe an
intermediate state during the operation.
\subsection{How they differ from ordinary variables}
With a normal variable, the innocent-looking increment \code{i++} is actually
\emph{three} separate CPU instructions:
\begin{enumerate}[leftmargin=2em]
\item \textbf{Read} the current value of \code{i} into a register.
\item \textbf{Add 1} to that register value.
\item \textbf{Write} the result back to \code{i}.
\end{enumerate}
If two threads execute \code{i++} concurrently, both may read the same value,
both add 1, and both write back the same result -- one increment is silently
lost.
This is a \emph{race condition}.
An atomic variable uses a hardware-level instruction called
\textbf{Compare-And-Swap (CAS)}, which reads, compares, and writes in a single
CPU operation that cannot be interrupted.
If another thread changed the value in between, the CAS fails and the operation
retries -- but no update is ever lost.
\subsection{Key difference summary}
\begin{center}
\begin{tabular}{>{\bfseries}lll}
\toprule
Property & Ordinary variable & Atomic variable \\
\midrule
Thread-safe reads/writes & No & Yes \\
Compound operations (e.g.\ \code{i++}) & Not atomic & Atomic (CAS) \\
Requires explicit lock & Yes & No \\
Performance under low contention & Faster & Comparable \\
\bottomrule
\end{tabular}
\end{center}
% ════════════════════════════════════════════════════════════════════════════
\section{Classes from \code{java.util.concurrent.atomic}}
% ════════════════════════════════════════════════════════════════════════════
The \code{java.util.concurrent.atomic} package provides lock-free, thread-safe
operations for a range of data types.
Four commonly used classes are listed below.
\begin{center}
\begin{tabular}{>{\bfseries}ll}
\toprule
Class & What it wraps \\
\midrule
\code{AtomicInteger} & A single \code{int} value \\
\code{AtomicLong} & A single \code{long} value \\
\code{AtomicBoolean} & A single \code{boolean} flag \\
\code{AtomicReference<V>} & A reference to any object of type \code{V} \\
\bottomrule
\end{tabular}
\end{center}
\subsection{Typical use case -- \code{AtomicInteger} as a shared request counter}
Imagine a web server where many threads handle HTTP requests simultaneously.
All of them must increment a shared request counter.
Using \code{AtomicInteger} avoids a \code{synchronized} block while still
guaranteeing that every request is counted exactly once:
\begin{lstlisting}[caption={Shared request counter with AtomicInteger}]
import java.util.concurrent.atomic.AtomicInteger;
public class RequestCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void handleRequest() {
int id = count.incrementAndGet(); // atomic: no lock needed
System.out.println("Handling request #" + id);
}
public int getTotal() {
return count.get();
}
}
\end{lstlisting}
Because \code{incrementAndGet()} is a single atomic CAS instruction, hundreds
of threads can call it simultaneously without any of them ever seeing a stale
or lost count.
% ════════════════════════════════════════════════════════════════════════════
\section{Locks vs.\ Atomic Variables}
% ════════════════════════════════════════════════════════════════════════════
Both locks and atomic variables protect shared data from concurrent access, but
they are suited to different scenarios.
\subsection{When a lock is the better choice}
\begin{itemize}[leftmargin=2em]
\item \textbf{Multiple variables must stay consistent together.}
Atomic variables protect exactly one value at a time.
If you need to update an account's \code{balance} and \code{lastModified}
timestamp as one unit, only a lock can guard both fields atomically.
\item \textbf{Complex conditions and waiting.}
Locks support \code{Condition} objects (\code{await}/\code{signal}),
letting a thread block until some business invariant is satisfied
(e.g.\ ``wait until the queue is non-empty'').
Atomic variables provide no such mechanism.
\item \textbf{Long critical sections.}
If the guarded work is more than a single read-modify-write (e.g.\
several method calls or I/O), a lock is the natural fit.
CAS-based retry loops become expensive when the critical section is
long, because failed retries waste CPU cycles.
\end{itemize}
\subsection{When an atomic variable is the better choice}
\begin{itemize}[leftmargin=2em]
\item \textbf{Single-variable, simple operations.}
Counters, flags, and version numbers that are updated with one CAS
have lower overhead than acquiring and releasing a lock.
\item \textbf{High-read, low-write scenarios.}
\code{AtomicReference} allows a single writer to publish a new
immutable snapshot while many readers access the current reference
without any blocking.
\item \textbf{Non-blocking algorithms.}
Lock-free data structures (e.g.\ \code{ConcurrentLinkedQueue}) use
CAS to guarantee progress even if one thread is delayed, whereas a
lock-based structure can stall all other threads if the lock holder
is descheduled.
\end{itemize}
\begin{center}
\begin{tabular}{>{\bfseries}lll}
\toprule
Situation & Better choice & Reason \\
\midrule
Single counter / flag & Atomic variable & Low overhead, no blocking \\
Multiple fields as one unit & Lock & Atomicity across several variables \\
Conditional waiting & Lock & \code{Condition.await()} / \code{signal()} \\
Long critical section & Lock & CAS retries too costly \\
Non-blocking read-heavy & Atomic variable & Readers never block \\
\bottomrule
\end{tabular}
\end{center}
% ════════════════════════════════════════════════════════════════════════════
\section{Correct but Slow -- Scalability Limits Under High Contention}
% ════════════════════════════════════════════════════════════════════════════
A program can be completely free of race conditions and still perform poorly
when many threads compete for the same resources.
Correctness and scalability are independent properties.
\subsection{Why this happens}
When every thread must pass through the same lock, threads queue up waiting
even if they could logically run in parallel.
Adding more CPU cores does not help -- the bottleneck is the shared resource,
not raw compute power.
This matches \textbf{Amdahl's Law}: total speedup is bounded by the fraction
of work that \emph{cannot} be parallelised.
\subsection{Three concurrency factors that limit scalability}
\begin{enumerate}[leftmargin=2em,label=\textbf{\arabic*.}]
\item \textbf{Lock contention.}
A coarse-grained lock (one lock protecting many accounts, for example)
serialises all threads even when they are working on completely
independent data.
The more threads, the longer each one waits.
The solution is to use finer-grained locking -- one lock per account
instead of one global lock -- so threads that are not sharing data can
proceed concurrently.
\item \textbf{False sharing (cache-line contention).}
Modern CPUs cache memory in 64-byte \emph{cache lines}.
If two threads update logically unrelated variables that happen to
reside on the same cache line, each write by one thread invalidates
the other core's cached copy.
The hardware constantly transfers the cache line between cores -- a
hidden performance cost that grows with thread count even though there
is no logical data sharing between the threads.
\item \textbf{Memory bandwidth saturation.}
With many threads all reading and writing shared state, the memory bus
becomes a bottleneck.
Once the bus is saturated, adding more threads only increases waiting
time -- each thread must wait longer for its memory access to be
serviced.
Throughput plateaus or even decreases despite the increase in CPU
utilisation.
\end{enumerate}
% ════════════════════════════════════════════════════════════════════════════
\section{Why More Threads Do Not Always Improve Performance}
% ════════════════════════════════════════════════════════════════════════════
Intuitively, more threads should mean more parallelism and faster execution.
In practice, beyond a certain point additional threads hurt rather than help.
\subsection{Context switching}
When the number of threads exceeds the number of CPU cores, the operating
system must \emph{time-slice}: it saves the full register state of one thread
and loads the state of another many times per second.
Each switch has a non-trivial cost (typically a few microseconds).
With hundreds of threads competing for a small number of cores, the CPU can
spend a significant fraction of its time doing bookkeeping instead of useful
work.
\subsection{Lock contention}
More threads means more threads competing for the same lock simultaneously.
Each additional thread increases the average queue length at every lock, so
threads spend proportionally more time \emph{blocked} and less time running.
In extreme cases, throughput actually decreases because the contention overhead
outweighs any benefit from concurrency.
\subsection{Cache coherence}
Every CPU core maintains its own L1/L2 cache.
When one core writes to a shared variable, all other cores that cached that
value must \emph{invalidate} their copy (the MESI protocol).
With many threads spread across many cores, this cache-invalidation ``chatter''
becomes expensive: cores stall waiting for the authoritative copy to arrive
from another core's cache or from main memory.
\subsection{Synchronization overhead}
Every \code{lock()}/\code{unlock()} pair, every \code{synchronized} block, and
every \code{volatile} write inserts a \emph{memory fence} instruction.
Memory fences force the CPU to flush its store buffers and ensure all prior
writes are visible to other cores before proceeding.
The more threads, the more synchronisation events per unit of time, and the
more time spent on coordination rather than computation.
\subsection{Summary}
\begin{center}
\begin{tabular}{>{\bfseries}ll}
\toprule
Factor & Effect \\
\midrule
Context switching & CPU wastes time saving/restoring thread state \\
Lock contention & Threads queue up; throughput flattens or drops \\
Cache coherence & Cores stall waiting for invalidated cache lines \\
Sync overhead & Memory fences cost cycles per lock/volatile operation \\
\bottomrule
\end{tabular}
\end{center}
% ════════════════════════════════════════════════════════════════════════════
\section{Why Deadlocks Appear in Production but Not During Testing}
% ════════════════════════════════════════════════════════════════════════════
\subsection{A thread-scheduling perspective}
A deadlock requires a very specific simultaneous state: Thread~A holds
lock~1 and waits for lock~2, while Thread~B holds lock~2 and waits for lock~1
-- at the \emph{same instant}.
This is called a \emph{circular wait}.
In a typical test environment:
\begin{itemize}[leftmargin=2em]
\item The workload is small (few threads, few iterations).
\item The JVM is freshly started with warm-up behaviour.
\item The OS scheduler tends to run the same thread continuously for short
tasks, so the critical interleaving rarely occurs.
\end{itemize}
In production:
\begin{itemize}[leftmargin=2em]
\item There are many more threads running concurrently.
\item Higher load means more context switches per second.
\item Different hardware (more cores, different cache sizes) produces
different scheduling patterns.
\item The combination of load, hardware, and OS scheduler eventually
\emph{will} hit the exact timing that triggers the circular wait,
making it a statistical certainty over hours of operation.
\end{itemize}
\subsection{Two strategies to expose deadlocks during testing}
\begin{enumerate}[leftmargin=2em,label=\textbf{\arabic*.}]
\item \textbf{Stress testing with a start-gun latch.}
Create many more threads than CPU cores (e.g.\ 50--100 threads for
a 4-core machine), use a \code{CountDownLatch} to hold them all back,
then release them simultaneously with a single \code{countDown()}.
Every thread starts at the same instant, maximising contention and
forcing the scheduler to interleave lock acquisitions in ways that
almost never happen under light load.
A deadlock that requires simultaneous lock attempts becomes statistically
likely within seconds.
This is exactly the technique used in the provided
\code{BankAccountTransferDeadlockTest}:
\begin{lstlisting}[caption={Stress test with a start-gun latch}]
CountDownLatch startGun = new CountDownLatch(1);
// ... submit 50 000 tasks that each await(startGun) before transferring
startGun.countDown(); // all tasks released at once
\end{lstlisting}
\item \textbf{Inject random delays between lock acquisitions.}
Insert a short \code{Thread.sleep(randomMillis)} between acquiring
the first lock and trying to acquire the second.
This \emph{widens} the window during which another thread can step in,
acquire its own first lock (which is the second lock of the first
thread), and then attempt the lock that the first thread holds -- the
exact condition for a circular wait.
Even a 1--5 ms random delay dramatically increases the probability
of hitting the deadlock without requiring a huge thread count.
This technique is sometimes called \emph{sleep injection} or
\emph{timing perturbation}.
\end{enumerate}
% ════════════════════════════════════════════════════════════════════════════
\section*{Bonus: \code{AtomicInteger} vs.\ Plain \code{int} Race Condition}
\addcontentsline{toc}{section}{Bonus: AtomicInteger vs.\ Plain int Race Condition}
% ════════════════════════════════════════════════════════════════════════════
The program below creates 100 threads, each incrementing both a plain
\code{int} and an \code{AtomicInteger} 1\,000 times.
The expected final value is $100 \times 1000 = 100\,000$.
\begin{lstlisting}[caption={RaceConditionDemo.java}]
import java.util.concurrent.atomic.AtomicInteger;
public class RaceConditionDemo {
static int unsafeCounter = 0;
static AtomicInteger safeCounter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
int threadCount = 100;
int incrementsPerThread = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementsPerThread; j++) {
unsafeCounter++; // NOT atomic -- race condition!
safeCounter.incrementAndGet(); // atomic -- always correct
}
});
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
int expected = threadCount * incrementsPerThread;
System.out.println("Expected : " + expected);
System.out.println("Unsafe int: " + unsafeCounter
+ (unsafeCounter == expected ? " (correct)" : " <-- LOST UPDATES!"));
System.out.println("AtomicInt : " + safeCounter.get()
+ (safeCounter.get() == expected ? " (correct)" : " <-- wrong!"));
}
}
\end{lstlisting}
\subsection*{Sample output}
\begin{lstlisting}[language={},numbers=none,backgroundcolor=\color{codebg}]
Expected : 100000
Unsafe int: 94371 <-- LOST UPDATES!
AtomicInt : 100000 (correct)
\end{lstlisting}
\subsection*{Why the unsafe counter is wrong}
\code{unsafeCounter++} compiles to three bytecode instructions: \code{getfield},
\code{iadd}, \code{putfield}.
Between any two of these instructions the thread scheduler can switch to
another thread.
If two threads both read the same value (say 5\,000), both add 1, and both
write back 5\,001, one increment vanishes entirely.
Under 100 threads this happens thousands of times, so the final value is
typically several thousand less than 100\,000.
\code{AtomicInteger.incrementAndGet()} maps to a single \code{lock xadd} CPU
instruction (on x86) that is guaranteed to be indivisible.
No thread can read an intermediate state; every increment is counted
exactly once, and the result is always 100\,000.
% ── Footer note ──────────────────────────────────────────────────────────────
\vfill
\begin{center}
\small\color{gray}
Advanced Programming Assignment 9 \quad|\quad
Faraz Ardeh \quad|\quad
Shahid Beheshti University \quad|\quad
June 2026
\end{center}
\end{document}
BIN
View File
Binary file not shown.
@@ -1,18 +1,12 @@
package dev.banking.model; package dev.banking.model;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount { public class BankAccount {
private final int accountId; private final int accountId;
private long balance; private long balance;
private final ReentrantLock lock = new ReentrantLock();
/*
* Students may introduce additional fields
* such as:
* - Lock / ReentrantLock
* - ReadWriteLock
* - Object monitor
* - etc.
*/
public BankAccount(int accountId, long initialBalance) { public BankAccount(int accountId, long initialBalance) {
this.accountId = accountId; this.accountId = accountId;
@@ -23,56 +17,53 @@ public class BankAccount {
return accountId; return accountId;
} }
/*
* TODO:
* Return the current balance in a thread-safe way.
*
* Requirements:
* - Must be safe under concurrent reads/writes
* - Should not block unnecessarily if using read/write locks
*/
public long getBalance() { public long getBalance() {
throw new UnsupportedOperationException("TODO: implement thread-safe balance read"); lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
} }
/*
* TODO:
* Increase balance atomically.
*
* Requirements:
* - Must not lose updates under concurrency
*/
public void deposit(long amount) { public void deposit(long amount) {
throw new UnsupportedOperationException("TODO: implement thread-safe deposit"); lock.lock();
try {
balance += amount;
} finally {
lock.unlock();
}
} }
/*
* TODO:
* Decrease balance atomically.
*
* Requirements:
* - Must not cause race conditions
* - Negative balance handling is NOT required unless you decide
* to extend the system (optional)
*/
public void withdraw(long amount) { public void withdraw(long amount) {
throw new UnsupportedOperationException("TODO: implement thread-safe withdraw"); lock.lock();
try {
balance -= amount;
} finally {
lock.unlock();
}
} }
/* /*
* TODO: * Lock ordering by accountId prevents deadlock:
* Transfer money between two accounts atomically. * both A->B and B->A transfers always acquire the
* * lower-id lock first, so no circular wait can form.
* IMPORTANT REQUIREMENTS:
* - Must be atomic (no partial transfer)
* - Must be deadlock-free
* - Must protect both source and target accounts
*
* HINT:
* - Consider global lock ordering using accountId
* - Or tryLock with retry strategy
*/ */
public void transfer(BankAccount target, long amount) { public void transfer(BankAccount target, long amount) {
throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer"); BankAccount first = this.accountId < target.accountId ? this : target;
BankAccount second = this.accountId < target.accountId ? target : this;
first.lock.lock();
try {
second.lock.lock();
try {
this.balance -= amount;
target.balance += amount;
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
} }
} }