From bb8e84ecfaa663dc03577ae34a815e7b08ef6caa Mon Sep 17 00:00:00 2001 From: Arefe Date: Mon, 8 Jun 2026 16:09:57 +0330 Subject: [PATCH 1/2] complete theoretical part --- Answers.md | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 Answers.md diff --git a/Answers.md b/Answers.md new file mode 100644 index 0000000..f5732e5 --- /dev/null +++ b/Answers.md @@ -0,0 +1,182 @@ +### ⚛️ Atomic Variables & Synchronization + + +1. Atomic variables in Java are part of the `java.util.concurrent.atomic` package. +They provide lock‑free, thread‑safe operations on a single variable. +Their purpose is to allow read‑modify‑write operations to be performed +as a single, indivisible unit without using synchronized blocks or explicit locks. +2. + * `AtomicInteger` – for `int` values + * `AtomicLong` – for `long` values + * `AtomicBoolean` – for `boolean` values + * `AtomicReference` – for object references (any type). +
+ Typical use case for `AtomicInteger`: A request counter in a web server. + Each incoming request increments the counter to generate a unique ID or to count total requests. + Multiple threads (handling requests concurrently) can safely call `incrementAndGet()` without any locks. +3. Locks and atomic variables are both tools for managing thread safety, +but they differ significantly in how they work and when you should use them. +
+ * Locks can protect arbitrarily large sections of code and can coordinate access to multiple + variables simultaneously. For example, if you need to update two bank accounts in a single atomic + operation, you must use a lock. Atomic variables, on the other hand, only cover a single variable. + You cannot atomically update two independent `AtomicInteger` objects together without additional synchronization. + * Locks are blocking – when a thread cannot acquire a lock, it is suspended by the operating system and later + woken up, which causes context switches and overhead. Atomic variables are non‑blocking; they use hardware‑level + Compare‑And‑Swap (CAS) instructions. If an atomic operation fails because another thread modified the variable, + it simply retries immediately (spins) without leaving the CPU. + * Because locks can be held while waiting for other locks, they can cause deadlocks if not used carefully. + Atomic variables never cause deadlocks because there is no waiting for locks – each operation either + succeeds immediately or retries. + * For simple operations like incrementing a counter, atomic variables are usually much faster than locks, + especially when contention is low to moderate. However, under extremely high contention (many threads pounding + the same variable), atomic variables may suffer from excessive retry spinning, and a well‑tuned lock might perform + better. For long critical sections (e.g., many lines of code, I/O, or complex updates), locks are more efficient + because spinning would waste CPU cycles. +* **When is a lock a better choice?** + + -When you need to atomically update multiple variables that belong together (e.g., transferring money between accounts). + + -When the critical section is long or contains blocking operations (like network calls or file I/O). + + -When you need explicit waiting and notification. + +* **When is an atomic variable a better choice?** + + -For simple, single‑variable operations such as counters, sequence generators, or status flags. + + -When you want lock‑free code that cannot deadlock. + + -For high‑frequency updates where lock overhead would become a bottleneck (e.g., statistics collection, request counters). + + + +#### 🎯Bonus Task: +```java +public class raceCondition +{ + private static int plainCounter = 0; + private static AtomicInteger atomicCounter = new AtomicInteger(0); + + public static void main(String[] args) throws InterruptedException + { + final int THREAD_COUNT = 10; + final int INCREMENTS_PER_THREAD = 1000; + + Thread[] threads = new Thread[THREAD_COUNT]; + + for (int i = 0; i < THREAD_COUNT; i++) + { + threads[i] = new Thread(()-> { + for (int j = 0; j < INCREMENTS_PER_THREAD; j++) + { + plainCounter++; + atomicCounter.incrementAndGet(); + } + }); + } + + for (int i = 0;i < THREAD_COUNT; i++) + { + threads[i].start(); + } + for (int i = 0;i < THREAD_COUNT; i++) + { + threads[i].join(); + } + + int expected = THREAD_COUNT*INCREMENTS_PER_THREAD; + System.out.println("expected: "+expected); + System.out.println("int: "+plainCounter); + System.out.println("atomic: "+atomicCounter); + + } +} +``` +sample output: +``` +expected: 10000 +int: 8880 +atomic: 10000 +``` + +--- +### 🔒 Locks & Concurrent Design + +4. **Explanation:** + Even if a program has no race conditions, it can still suffer from poor performance when many threads compete +for the same resources. High contention means many threads try to access the same shared data or locks at the same time. +While correctness is preserved, throughput can drop dramatically because threads spend more time waiting, retrying, +or invalidating caches than doing useful work. + + **Three concurrency‑related factors that limit scalability:** + + * **Lock contention** + + If a program uses a single coarse‑grained lock (e.g., synchronizing the whole method), only one thread can + execute the critical section at a time. All other threads queue up and block. As more threads are added, the queue + grows, but the throughput cannot exceed the rate at which the lock is released and reacquired. This turns a + concurrent program into essentially a sequential one for that resource, creating a scalability bottleneck. + + * **Cache coherence traffic** + + On modern multi‑core CPUs, each core has its own cache. When multiple threads repeatedly read and write to the + same memory location (even with atomic operations), the caches must stay consistent. The hardware uses a cache + coherence protocol. Every write to a shared variable invalidates the cache line in all other cores, forcing them to + reload from main memory or a shared cache. Under high contention, this causes a storm of invalidations and cache + misses, increasing memory latency and reducing performance even without explicit locks. + + * **False sharing** + + False sharing occurs when two or more threads modify different variables that happen to reside on the same + cache line (typically 64 bytes). Although the threads do not share the same logical variable, the cache coherence + protocol treats the entire line as shared. When one thread updates its variable, the cache line is invalidated on + other cores, causing unnecessary reloads. This can slow down seemingly independent threads, and the problem worsens + with more threads because the probability of cache line overlaps increases. + + +5. * **Context switching overhead** + + The operating system can run only as many threads as there are hardware cores (or hardware threads like +Hyper‑Threading). When the number of active threads exceeds the number of cores, the OS must constantly pause one thread +and switch to another. A context switch involves saving and restoring register states, updating memory management +structures, and flushing parts of the pipeline and caches. Each switch costs microseconds – small per switch, but when +thousands of switches happen per second, total overhead becomes significant, reducing useful work throughput. + * **Contention for shared resources** + + As more threads compete for the same locks, memory, or I/O channels, the fraction of time spent waiting + (blocking) increases. Throughput does not increase linearly and eventually saturates. Contention on a popular lock + can cause the system to spend most of its time in the operating system scheduler and in lock‑handling code, leading + to severe performance collapse. + * **Cache coherence** + + More threads mean more cores reading and writing to shared data. Every write to a shared variable triggers cache + coherency traffic (invalidations, bus transactions). This traffic increases with the square of the number of + contending cores in some cases. Moreover, all cores share the same memory bus. When many threads access memory + heavily, the bus becomes a bottleneck, and memory latency increases due to queuing delays. + * **Synchronization overhead** + + Every locking operation (`synchronized`, `ReentrantLock`, `Semaphore`) involves overhead: acquiring the lock, + possibly parking the thread, and later unparking it. Even lock‑free atomic operations under high contention cause + repeated retries, which burn CPU cycles without progressing. The overhead per operation grows, and total throughput can drop. + +--- +### ⚠️ Deadlocks +6. * **Why Deadlocks Often Appear Only in Production? (Thread‑Scheduling Perspective)** + + Deadlocks are notoriously hard to reproduce during testing because they depend on specific interleavings of thread +execution – the exact order in which threads acquire locks. In a testing environment (e.g., with low load, few cores, or +deterministic scheduling), the probability of hitting the exact timing window where two threads hold locks in opposite +order is extremely low. + * **Two Strategies to Expose Deadlocks During Testing:** + * Stress Testing with Thread Interleaving Controllers (e.g., jcstress, Lincheck): + Use tools that systematically explore thread interleavings. For example, Java Concurrency Stress (jcstress) + generates many schedules, including rare ones. Alternatively, ConcurrentLinkedDeque test harnesses or Lincheck + (from Kotlin) can be used. A simpler approach: in a test, repeatedly run a scenario with many threads and use + Thread.yield() or Thread.sleep(1) at strategic points to increase the chance of switching contexts in the middle of + lock acquisition. + * Inject Artificial Delays and Random Preemption Points: + Within the critical sections, insert small random sleeps (Thread.sleep(1)) or Thread.yield() right after + acquiring the first lock but before acquiring the second lock. This greatly increases the chance of interleaving. + Use a randomised test runner that loops the same test thousands of times with different random seeds. Also, run + tests on machines with more CPU cores and under load to make scheduling less predictable. From 847910135ca41231b1921fadc36d1b95cee8af20 Mon Sep 17 00:00:00 2001 From: Arefe Date: Thu, 11 Jun 2026 15:27:27 +0330 Subject: [PATCH 2/2] complete practical part --- .idea/.gitignore | 10 ++ .idea/compiler.xml | 13 ++ .idea/encodings.xml | 7 ++ .idea/jarRepositories.xml | 20 ++++ .idea/misc.xml | 12 ++ .idea/vcs.xml | 7 ++ pom.xml | 8 ++ .../java/dev/banking/model/BankAccount.java | 112 ++++++++++-------- 8 files changed, 139 insertions(+), 50 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ab1f416 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ 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..f4fe6ed --- /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..eba6e1f --- /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/pom.xml b/pom.xml index 33ea6f3..a768988 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,14 @@ 5.12.2 test + + + org.junit.platform + junit-platform-launcher + 1.12.2 + test + + diff --git a/src/main/java/dev/banking/model/BankAccount.java b/src/main/java/dev/banking/model/BankAccount.java index 745ede2..8277fa6 100644 --- a/src/main/java/dev/banking/model/BankAccount.java +++ b/src/main/java/dev/banking/model/BankAccount.java @@ -1,18 +1,15 @@ package dev.banking.model; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + 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 ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final Lock readLock = rwLock.readLock(); + private final Lock writeLock = rwLock.writeLock(); public BankAccount(int accountId, long initialBalance) { this.accountId = accountId; @@ -23,56 +20,71 @@ 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"); + readLock.lock(); + try + { + return balance; + } finally + { + readLock.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"); + writeLock.lock(); + try + { + balance += amount; + } finally + { + writeLock.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"); + writeLock.lock(); + try + { + balance -= amount; + } finally + { + writeLock.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 - */ public void transfer(BankAccount target, long amount) { - throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer"); + if (target == this) + { + return; + } + BankAccount first = this; + BankAccount second = target; + + if (this.getAccountId() > target.getAccountId()) + { + first = target; + second = this; + } + + + first.writeLock.lock(); + try + { + second.writeLock.lock(); + try + { + //"first" and "second":only for the order of locking to prevent deadlock (have nothing to do with the direction of the transfer) + this.balance -= amount; + target.balance += amount; + } finally + { + second.writeLock.unlock(); + } + } finally + { + first.writeLock.unlock(); + } } + } \ No newline at end of file