Implement thread-safe BankAccount operations #1

Merged
Aryan merged 2 commits from develop into main 2026-07-04 07:46:16 +00:00
8 changed files with 190 additions and 50 deletions
+10
View File
@@ -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/
+13
View File
@@ -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>
+7
View File
@@ -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>
+20
View File
@@ -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://maven.devneeds.ir/" />
</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>
+12
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+73
View File
@@ -0,0 +1,73 @@
## Question 1
1 - What are atomic variables?
Atomic variables are variables that provide thread-safe operations without using explicit locks. Operations such as incrementing or updating a value are performed atomically, meaning they cannot be interrupted by other threads.
Ordinary variables do not provide this guarantee. For example, the expression `counter++` consists of multiple steps, and different threads may interfere with each other, causing race conditions.
Atomic variables are mainly used to safely share data between multiple threads.
## Question 2
2 - Name at least four classes from the java.util.concurrent.atomic package that provide atomic operations for different data types.
Some classes from the `java.util.concurrent.atomic` package are:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference
A common use case for `AtomicInteger` is a shared counter. Multiple threads can safely increment the counter without using locks.
## Question 3
3 - Compare locks with atomic variables.
Atomic variables are usually faster and simpler for operations on a single variable. They have lower overhead and do not require explicit locking.
Locks are better when multiple operations or multiple shared variables must be protected together. They provide more flexibility but usually have higher overhead.
Use atomic variables for simple updates such as counters. Use locks for complex critical sections that involve several operations.
## Question 4
4 - A program is completely free of race conditions but still performs poorly under high contention.
A program can be free of race conditions and still perform poorly because correctness does not guarantee scalability.
Some factors that limit performance are:
1. Lock contention, where many threads wait for the same lock.
2. Thread blocking, which reduces parallel execution.
3. Synchronization overhead, which adds extra work for coordinating threads.
As a result, the program remains correct but may not scale well under heavy load.
## Question 5
5 - Many concurrent systems experience performance degradation as the number of threads increases.
Adding more threads does not always improve performance.
- Context switching takes CPU time when the operating system switches between threads.
- Contention happens when many threads compete for the same resources.
- Cache coherence creates extra work for processors to keep shared data consistent.
- Synchronization overhead increases because locks and coordination mechanisms require additional processing.
Because of these costs, too many threads can actually reduce performance.
## Question 6
6 - Deadlocks often only appear in production, not during testing.
Deadlocks often depend on specific thread schedules. During testing, the required scheduling order may never occur. In production, higher load and different timing make deadlocks more likely.
Two ways to expose deadlocks during testing are:
1. Stress testing with many threads and repeated executions.
2. Adding random delays to create different thread interleavings.
These techniques increase the chance of reproducing deadlock situations before deployment.
@@ -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,61 @@ 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
*/
public void transfer(BankAccount target, long amount) {
throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer");
BankAccount first;
BankAccount second;
if (this.accountId < target.accountId) {
first = this;
second = target;
} else {
first = target;
second = this;
}
first.lock.lock();
second.lock.lock();
try {
this.balance -= amount;
target.balance += amount;
} finally {
second.lock.unlock();
first.lock.unlock();
}
}
}