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/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.
diff --git a/pom.xml b/pom.xml
index 33ea6f3..a768988 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,14 @@
5.12.2test
+
+
+ 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