Assignment 9 – Theoretical Questions
This commit is contained in:
Generated
+10
@@ -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
|
||||||
Generated
+13
@@ -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>
|
||||||
Generated
+7
@@ -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>
|
||||||
Generated
+20
@@ -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>
|
||||||
Generated
+12
@@ -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
@@ -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.
@@ -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}
|
||||||
Vendored
BIN
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user