feat : implement BankAccount methods + Solutions.md file (including bonus question) #1
+148
@@ -0,0 +1,148 @@
|
||||
1 - What are atomic variables?
|
||||
|
||||
Atomic variables are variables that support thread-safe operations without using explicit locks.
|
||||
|
||||
They ensure that operations such as incrementing, decrementing, or updating a value are performed as a single indivisible action.
|
||||
|
||||
Difference from ordinary variables:
|
||||
|
||||
Ordinary variable: Multiple threads can modify it simultaneously, causing race conditions.
|
||||
Atomic variable: Operations are performed atomically, preventing race conditions for single-variable updates.
|
||||
|
||||
2 - Name at least four classes from the java.util.concurrent.atomic package that provide atomic operations for different data types.
|
||||
|
||||
AtomicInteger
|
||||
AtomicLong
|
||||
AtomicBoolean
|
||||
AtomicReference<T>
|
||||
|
||||
Typical use case for AtomicInteger:
|
||||
AtomicInteger is commonly used as a thread-safe counter when multiple threads need to increment a shared value concurrently.
|
||||
|
||||
3 - Compare locks with atomic variables.
|
||||
|
||||
atomic variables:
|
||||
|
||||
Advantages:
|
||||
|
||||
Faster than locks for simple operations.
|
||||
Non-blocking.
|
||||
Lower overhead.
|
||||
|
||||
Best for:
|
||||
|
||||
Counters.
|
||||
Flags.
|
||||
Single-variable updates.
|
||||
Locks
|
||||
|
||||
locks:
|
||||
|
||||
Advantages:
|
||||
Can protect multiple variables together.
|
||||
Support complex critical sections.
|
||||
|
||||
Best for:
|
||||
|
||||
Operations involving multiple shared objects.
|
||||
Complex business logic requiring mutual exclusion.
|
||||
|
||||
Summary:
|
||||
|
||||
Use atomic variables for simple thread-safe updates.
|
||||
Use locks when multiple operations must be performed as one atomic unit.
|
||||
|
||||
Bonus Task
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AtomicDemo
|
||||
{
|
||||
static int normalCounter = 0;
|
||||
static AtomicInteger atomicCounter = new AtomicInteger(0);
|
||||
|
||||
public static void main(String[] args) throws InterruptedException
|
||||
{
|
||||
|
||||
int numThreads = 10;
|
||||
int incrementsPerThread = 100000;
|
||||
|
||||
Thread[] threads = new Thread[numThreads];
|
||||
|
||||
for (int i = 0; i < numThreads; i++) {
|
||||
threads[i] = new Thread(() -> {
|
||||
for (int j = 0; j < incrementsPerThread; j++) {
|
||||
normalCounter++;
|
||||
atomicCounter.incrementAndGet();
|
||||
}
|
||||
});
|
||||
|
||||
threads[i].start();
|
||||
}
|
||||
|
||||
for (Thread thread : threads) {
|
||||
thread.join();
|
||||
}
|
||||
|
||||
System.out.println("Normal Counter: " + normalCounter);
|
||||
System.out.println("Atomic Counter: " + atomicCounter.get());
|
||||
}
|
||||
}
|
||||
|
||||
Expected Result:
|
||||
|
||||
Normal Counter: 823451 (varies)
|
||||
Atomic Counter: 1000000
|
||||
|
||||
The normal counter may be incorrect because multiple threads overwrite each other's updates. The atomic counter always produces the correct result.
|
||||
|
||||
4 - A program is completely free of race conditions but still performs poorly under high contention.
|
||||
Explain how this situation can occur.
|
||||
|
||||
A program can be completely correct yet still perform poorly because threads spend too much time waiting for shared resources.
|
||||
|
||||
Three factors that limit scalability:
|
||||
|
||||
Lock Contention
|
||||
Many threads compete for the same lock and must wait.
|
||||
|
||||
Synchronization Overhead
|
||||
Acquiring and releasing locks consumes CPU time.
|
||||
|
||||
Thread Blocking
|
||||
Threads frequently pause while waiting for locks or resources, reducing parallelism.
|
||||
|
||||
Correctness is guaranteed, but throughput decreases as contention increases.
|
||||
|
||||
5 - Many concurrent systems experience performance degradation as the number of threads increases.
|
||||
Explain why adding more threads does not always improve performance.
|
||||
|
||||
The CPU must save and restore thread state when switching between threads. Too many threads increase this overhead.
|
||||
|
||||
Contention
|
||||
More threads compete for the same locks and resources, causing waiting.
|
||||
|
||||
Cache Coherence
|
||||
When multiple CPUs modify shared data, cache contents must be synchronized, creating extra communication overhead.
|
||||
|
||||
Synchronization Overhead
|
||||
Locks, atomic operations, and coordination mechanisms consume time and reduce the benefits of parallel execution.
|
||||
|
||||
Conclusion: After a certain point, adding more threads increases overhead more than useful work, causing performance to stagnate or even decrease.
|
||||
|
||||
6 - Deadlocks often only appear in production, not during testing.Explain why this might happen from a thread-scheduling perspective.
|
||||
|
||||
Deadlocks depend on the exact timing and scheduling of threads.
|
||||
During testing, threads may execute in a favorable order and never enter the deadlock state. In production, different workloads, hardware, and timing conditions can cause the problematic scheduling sequence to occur.
|
||||
|
||||
Two ways to expose deadlocks during testing
|
||||
|
||||
1. Increase concurrency
|
||||
Run with many threads.execute tests repeatedly under heavy load.
|
||||
|
||||
|
||||
2. Introduce timing variations
|
||||
Add random delays (Thread.sleep()).
|
||||
Use stress-testing tools to force different thread interleavings.
|
||||
|
||||
These techniques increase the chance that threads acquire locks in a problematic order, revealing deadlocks before deployment.
|
||||
@@ -1,5 +1,8 @@
|
||||
package dev.banking.model;
|
||||
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
public class BankAccount {
|
||||
|
||||
private final int accountId;
|
||||
@@ -13,6 +16,7 @@ public class BankAccount {
|
||||
* - Object monitor
|
||||
* - etc.
|
||||
*/
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
public BankAccount(int accountId, long initialBalance) {
|
||||
this.accountId = accountId;
|
||||
@@ -23,56 +27,96 @@ 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
|
||||
/**
|
||||
* Thread-safe balance read.
|
||||
*/
|
||||
public long getBalance() {
|
||||
throw new UnsupportedOperationException("TODO: implement thread-safe balance read");
|
||||
|
||||
lock.readLock().lock();
|
||||
|
||||
try
|
||||
{
|
||||
return balance;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
* Increase balance atomically.
|
||||
*
|
||||
* Requirements:
|
||||
* - Must not lose updates under concurrency
|
||||
/**
|
||||
* Thread-safe deposit.
|
||||
*/
|
||||
public void deposit(long amount) {
|
||||
throw new UnsupportedOperationException("TODO: implement thread-safe deposit");
|
||||
|
||||
lock.writeLock().lock();
|
||||
|
||||
try
|
||||
{
|
||||
balance += amount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.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)
|
||||
/**
|
||||
* Thread-safe withdrawal.
|
||||
*/
|
||||
public void withdraw(long amount) {
|
||||
throw new UnsupportedOperationException("TODO: implement thread-safe withdraw");
|
||||
|
||||
lock.writeLock().lock();
|
||||
|
||||
try
|
||||
{
|
||||
balance -= amount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
* Transfer money between two accounts atomically.
|
||||
/**
|
||||
* Atomic deadlock-free transfer.
|
||||
*
|
||||
* 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 rule:
|
||||
* Always acquire the lock of the account with the
|
||||
* smaller accountId first.
|
||||
*/
|
||||
public void transfer(BankAccount target, long amount) {
|
||||
throw new UnsupportedOperationException("TODO: implement atomic deadlock-free transfer");
|
||||
|
||||
if (target == null)
|
||||
throw new IllegalArgumentException("Target account cannot be null.");
|
||||
|
||||
if (this == target)
|
||||
return;
|
||||
|
||||
BankAccount first = this.accountId < target.accountId ? this : target;
|
||||
|
||||
BankAccount second = this.accountId < target.accountId ? target : this;
|
||||
|
||||
first.lock.writeLock().lock();
|
||||
|
||||
try
|
||||
{
|
||||
second.lock.writeLock().lock();
|
||||
|
||||
try
|
||||
{
|
||||
this.balance -= amount;
|
||||
target.balance += amount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
second.lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
first.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user