Merge pull request 'Develop' (#1) from develop into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
Generated
+10
@@ -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/
|
||||||
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="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>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="https://maven.myket.ir/" />
|
||||||
|
</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" 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>
|
||||||
+182
@@ -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<V>` – for object references (any type).
|
||||||
|
</br>
|
||||||
|
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.
|
||||||
|
</br>
|
||||||
|
* 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.
|
||||||
@@ -21,6 +21,14 @@
|
|||||||
<version>5.12.2</version>
|
<version>5.12.2</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.platform</groupId>
|
||||||
|
<artifactId>junit-platform-launcher</artifactId>
|
||||||
|
<version>1.12.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
package dev.banking.model;
|
package dev.banking.model;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
public class BankAccount {
|
public class BankAccount {
|
||||||
|
|
||||||
private final int accountId;
|
private final int accountId;
|
||||||
private long balance;
|
private long balance;
|
||||||
|
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
|
||||||
/*
|
private final Lock readLock = rwLock.readLock();
|
||||||
* Students may introduce additional fields
|
private final Lock writeLock = rwLock.writeLock();
|
||||||
* 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 +20,71 @@ 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");
|
readLock.lock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return balance;
|
||||||
|
} finally
|
||||||
|
{
|
||||||
|
readLock.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");
|
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) {
|
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) {
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user