HW-9 #1

Open
avasanatkar wants to merge 1 commits from develop into main
8 changed files with 226 additions and 7 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" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK" />
</project>
Generated
+7
View File
@@ -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>
+118
View File
@@ -0,0 +1,118 @@
# Answers
## Question 1 — Atomic Variables
Atomic variables are variables that support thread-safe operations without using locks.
When multiple threads try to do something like `i++` on a normal variable, this operation
actually has three steps: read, add, write. Another thread can jump in between these steps
and cause wrong results. Atomic variables do this whole thing in one unbreakable step using
a CPU instruction called CAS (Compare And Swap), so no thread can interrupt in the middle.
## Question 2 — Atomic Classes
Four classes from java.util.concurrent.atomic:
- AtomicInteger
- AtomicLong
- AtomicBoolean
- AtomicReference
Use case for AtomicInteger:
Counting how many transactions have been processed across multiple threads.
Instead of putting a lock around a simple counter, we can call
`atomicCounter.incrementAndGet()` which is faster and doesn't block threads.
## Question 3 — Locks vs Atomic Variables
Atomic variables are better when we only need to update a single variable safely,
like a counter or a flag. They are faster because they don't block other threads.
Locks are better when we need to update multiple variables together and all of them
must change as one unit. For example in a bank transfer, we need to subtract from one
account and add to another — both must happen together, so we use locks.
## Question 4 — Good Correctness but Bad Performance
A program can be completely correct with no race conditions but still be slow because:
1. Lock contention: if many threads want the same lock at the same time, they all have
to wait in line. Only one runs while the rest are stuck doing nothing.
2. False sharing: two threads might be working on different variables, but those variables
are stored next to each other in CPU cache. When one thread changes its variable, the CPU
forces the other thread to reload its cache even though nothing it cares about changed.
3. Coarse-grained locking: using one big lock for everything means even operations that
have nothing to do with each other have to wait. For example depositing into account A
and account B could happen at the same time, but a global lock prevents that.
## Question 5 — More Threads Doesn't Always Mean Faster
- Context switching: when there are more threads than CPU cores, the OS keeps switching
between them. Each switch takes time and does zero useful work.
- Contention: more threads fighting over the same lock means longer wait times.
At some point adding threads just makes the queue longer, not the work faster.
- Cache coherence: each CPU core has its own cache. When a shared variable changes,
all cores must update their copy. With many threads on many cores, this communication
becomes a bottleneck.
- Synchronization overhead: every lock and unlock has a cost. With too many threads,
the time spent on synchronization can be more than the actual work being done.
## Question 6 — Deadlocks in Production but Not in Testing
Deadlocks need a very specific order of events to happen. Thread1 must lock A at exactly
the moment Thread2 locks B, and then both try to get each other's lock. In testing,
the system is usually under low load with few threads, so this exact timing almost never
occurs. In production with thousands of concurrent threads, the chances of hitting that
exact bad sequence become much higher.
Two strategies to expose deadlocks during testing:
1. Stress testing: run tests with a very large number of threads doing random transactions
at the same time. The more threads competing, the higher the chance of triggering the
exact bad interleaving that causes a deadlock.
2. Inserting Thread.sleep() inside critical sections during tests: this artificially
slows down the thread right in the middle of acquiring locks, making it much more likely
that another thread will jump in and create the deadlock scenario.
## Bonus — AtomicInteger vs Normal int
```java
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 threadCount = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
normalCounter++;
atomicCounter.incrementAndGet();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
System.out.println("Expected: " + (threadCount * 1000));
System.out.println("Normal int result: " + normalCounter);
System.out.println("AtomicInteger result: " + atomicCounter.get());
}
}
```
The normal int will show a wrong number because threads interfere with each other.
The AtomicInteger will always show the correct result of 1000000.
@@ -1,10 +1,10 @@
package dev.banking.model;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccount {
private final int accountId;
private long balance;
private final ReentrantLock lock = new ReentrantLock();
/*
* Students may introduce additional fields
* such as:
@@ -22,7 +22,6 @@ public class BankAccount {
public int getAccountId() {
return accountId;
}
/*
* TODO:
* Return the current balance in a thread-safe way.
@@ -32,7 +31,13 @@ public class BankAccount {
* - 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();
}
}
/*
@@ -42,8 +47,14 @@ public class BankAccount {
* 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();
}
}
/*
@@ -55,8 +66,14 @@ public class BankAccount {
* - 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();
}
}
/*
@@ -72,7 +89,22 @@ public class BankAccount {
* - 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 = this.accountId < target.accountId ? this : target;
BankAccount second = this.accountId < target.accountId ? target : this;
first.lock.lock();
try {
second.lock.lock();
try {
this.balance -= amount;
target.balance += amount;
} finally {
second.lock.unlock();
}
} finally {
first.lock.unlock();
}
}
}