diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ea7b98e Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..812c3f9 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..f88c0a7 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Theoretical Questions.pdf b/Theoretical Questions.pdf new file mode 100644 index 0000000..dc09a9c Binary files /dev/null and b/Theoretical Questions.pdf differ diff --git a/Theoretical Questions.tex b/Theoretical Questions.tex new file mode 100644 index 0000000..cfeaa6a --- /dev/null +++ b/Theoretical Questions.tex @@ -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} & 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} diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000..5a8ec8c Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/java/dev/banking/model/BankAccount.java b/src/main/java/dev/banking/model/BankAccount.java index 745ede2..721da84 100644 --- a/src/main/java/dev/banking/model/BankAccount.java +++ b/src/main/java/dev/banking/model/BankAccount.java @@ -1,18 +1,12 @@ package dev.banking.model; +import java.util.concurrent.locks.ReentrantLock; + public class BankAccount { private final int accountId; private long balance; - - /* - * Students may introduce additional fields - * such as: - * - Lock / ReentrantLock - * - ReadWriteLock - * - Object monitor - * - etc. - */ + private final ReentrantLock lock = new ReentrantLock(); public BankAccount(int accountId, long initialBalance) { this.accountId = accountId; @@ -23,56 +17,53 @@ public class BankAccount { 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() { - 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) { - 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) { - throw new UnsupportedOperationException("TODO: implement thread-safe withdraw"); + lock.lock(); + try { + balance -= amount; + } finally { + lock.unlock(); + } } /* - * TODO: - * Transfer money between two accounts atomically. - * - * 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 + * Lock ordering by accountId prevents deadlock: + * both A->B and B->A transfers always acquire the + * lower-id lock first, so no circular wait can form. */ 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(); + } } -} \ No newline at end of file +}