2 Commits
2 changed files with 228 additions and 36 deletions
+148
View File
@@ -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();
}
}
}