develop
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="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>
|
||||
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="21" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# Ninth Assignment — Advanced Multithreading
|
||||
|
||||
## Theoretical Questions
|
||||
|
||||
---
|
||||
|
||||
## 1. What are atomic variables?
|
||||
|
||||
Atomic variables are variables that support operations which happen as one indivisible step. This means that other threads cannot observe the operation in a partially completed state.
|
||||
|
||||
They are used to safely update simple shared values without using explicit locks. For example, `counter++` on a normal `int` is not atomic because it includes reading, incrementing, and writing the value back. If multiple threads do this at the same time, some updates may be lost.
|
||||
|
||||
Using `AtomicInteger` solves this problem:
|
||||
atomicCounter.incrementAndGet();
|
||||
|
||||
text
|
||||
|
||||
This operation is atomic, so concurrent increments are handled correctly.
|
||||
|
||||
Ordinary variables do not protect against race conditions, while atomic variables provide thread-safe operations such as increment, decrement, and compare-and-set.
|
||||
|
||||
---
|
||||
|
||||
## 2. Name at least four classes from the `java.util.concurrent.atomic` package
|
||||
|
||||
Some classes from this package are:
|
||||
|
||||
- AtomicInteger
|
||||
- AtomicLong
|
||||
- AtomicBoolean
|
||||
- AtomicReference<T>
|
||||
- AtomicIntegerArray
|
||||
- AtomicLongArray
|
||||
|
||||
A common use case for `AtomicInteger` is a thread-safe counter. For example:
|
||||
AtomicInteger requestCount = new AtomicInteger(0);
|
||||
|
||||
requestCount.incrementAndGet();
|
||||
|
||||
text
|
||||
|
||||
This avoids lost updates when many threads update the counter concurrently.
|
||||
|
||||
---
|
||||
|
||||
## 3. Compare locks with atomic variables
|
||||
|
||||
Atomic variables are useful for simple operations on one shared value, such as counters, flags, or ID generators. They are lightweight and usually do not require blocking threads.
|
||||
|
||||
Locks are better when the operation is more complex or involves multiple shared variables. For example, a bank transfer must subtract money from one account and add it to another as one atomic operation. A lock can protect the whole critical section.
|
||||
|
||||
Atomic variables are best for simple independent updates, while locks are better for multi-step operations or when several shared resources must remain consistent.
|
||||
|
||||
---
|
||||
|
||||
## Bonus Task: AtomicInteger vs normal int
|
||||
|
||||
The bonus task is implemented in:
|
||||
|
||||
`src/main/java/dev/banking/bonus/AtomicCounterDemo.java`
|
||||
|
||||
This program creates multiple threads. Each thread increments both a normal `int` counter and an `AtomicInteger`.
|
||||
|
||||
The normal `int` may produce an incorrect final value because `normalCounter++` is not atomic. Multiple threads can read the same value and overwrite each other’s updates.
|
||||
|
||||
`AtomicInteger.incrementAndGet()` performs the increment atomically, so its final value should match the expected number of increments.
|
||||
|
||||
---
|
||||
|
||||
## 4. A program is completely free of race conditions but still performs poorly under high contention
|
||||
|
||||
A program can be correct but still slow. Being free of race conditions guarantees correctness, but not good performance.
|
||||
|
||||
High contention happens when many threads frequently compete for the same shared resource. For example, using one global lock for all bank accounts prevents race conditions but allows only one operation at a time.
|
||||
|
||||
Factors that limit scalability include:
|
||||
|
||||
- Lock contention
|
||||
- Context switching
|
||||
- Cache coherence overhead
|
||||
- Synchronization overhead
|
||||
|
||||
So even correct synchronization can perform poorly if many threads compete for the same resource.
|
||||
|
||||
---
|
||||
|
||||
## 5. Why adding more threads does not always improve performance
|
||||
|
||||
Adding more threads improves performance only when there is enough independent work to run in parallel. If threads mostly wait for shared resources, performance may decrease.
|
||||
|
||||
Main reasons include:
|
||||
|
||||
- Context switching between many threads
|
||||
- Contention for locks or shared resources
|
||||
- Cache coherence overhead between CPU cores
|
||||
- Synchronization overhead from locks and atomic operations
|
||||
|
||||
Therefore, more threads do not automatically mean better performance.
|
||||
|
||||
---
|
||||
|
||||
## 6. Deadlocks often only appear in production, not during testing
|
||||
|
||||
Deadlocks depend on thread scheduling and timing. During testing, the exact order of operations that causes a deadlock may not happen.
|
||||
|
||||
For example, one thread may lock account A and wait for account B, while another thread locks account B and waits for account A.
|
||||
|
||||
In production systems there are more users, transactions, and CPU cores, which increases the number of possible thread schedules and makes rare deadlocks more likely.
|
||||
|
||||
Two ways to detect deadlocks during testing are:
|
||||
|
||||
- Stress testing with many threads and operations
|
||||
- Artificial delays to increase the chance of problematic interleavings
|
||||
|
||||
Other helpful methods include repeated tests, randomized workloads, timeouts, and thread dumps using tools like `jstack`.
|
||||
@@ -0,0 +1,45 @@
|
||||
package dev.banking.bonus;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AtomicCounterDemo
|
||||
{
|
||||
|
||||
private static int normalCounter = 0;
|
||||
private static final AtomicInteger atomicCounter = new AtomicInteger(0);
|
||||
|
||||
public static void main(String[] args) throws InterruptedException
|
||||
{
|
||||
int threadCount = 10;
|
||||
int incrementsPerThread = 100_000;
|
||||
|
||||
Thread[] threads = new Thread[threadCount];
|
||||
|
||||
for (int i = 0; i < threadCount; i++)
|
||||
{
|
||||
threads[i] = new Thread(() -> {
|
||||
for (int j = 0; j < incrementsPerThread; j++)
|
||||
{
|
||||
normalCounter++;
|
||||
atomicCounter.incrementAndGet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (Thread thread : threads)
|
||||
{
|
||||
thread.start();
|
||||
}
|
||||
|
||||
for (Thread thread : threads)
|
||||
{
|
||||
thread.join();
|
||||
}
|
||||
|
||||
int expected = threadCount * incrementsPerThread;
|
||||
|
||||
System.out.println("Expected value: " + expected);
|
||||
System.out.println("Normal int value: " + normalCounter);
|
||||
System.out.println("AtomicInteger value: " + atomicCounter.get());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package dev.banking.model;
|
||||
|
||||
public class BankAccount {
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class BankAccount
|
||||
{
|
||||
|
||||
private final int accountId;
|
||||
private long balance;
|
||||
@@ -14,12 +17,16 @@ public class BankAccount {
|
||||
* - etc.
|
||||
*/
|
||||
|
||||
public BankAccount(int accountId, long initialBalance) {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public BankAccount(int accountId, long initialBalance)
|
||||
{
|
||||
this.accountId = accountId;
|
||||
this.balance = initialBalance;
|
||||
}
|
||||
|
||||
public int getAccountId() {
|
||||
public int getAccountId()
|
||||
{
|
||||
return accountId;
|
||||
}
|
||||
|
||||
@@ -31,8 +38,18 @@ public class BankAccount {
|
||||
* - 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");
|
||||
|
||||
public long getBalance()
|
||||
{
|
||||
lock.lock();
|
||||
try
|
||||
{
|
||||
return balance;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -42,8 +59,17 @@ public class BankAccount {
|
||||
* Requirements:
|
||||
* - Must not lose updates under concurrency
|
||||
*/
|
||||
public void deposit(long amount) {
|
||||
throw new UnsupportedOperationException("TODO: implement thread-safe deposit");
|
||||
public void deposit(long amount)
|
||||
{
|
||||
lock.lock();
|
||||
try
|
||||
{
|
||||
balance += amount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -55,8 +81,16 @@ 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");
|
||||
public void withdraw(long amount)
|
||||
{
|
||||
lock.lock();
|
||||
try
|
||||
{
|
||||
balance -= amount;
|
||||
} finally
|
||||
{
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -72,7 +106,54 @@ 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");
|
||||
public void transfer(BankAccount target, long amount)
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Target account cannot be null");
|
||||
}
|
||||
|
||||
if (target == this)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.accountId == target.accountId)
|
||||
{
|
||||
throw new IllegalArgumentException("Different accounts cannot have the same accountId");
|
||||
}
|
||||
|
||||
BankAccount firstLockAccount;
|
||||
BankAccount secondLockAccount;
|
||||
|
||||
if (this.accountId < target.accountId)
|
||||
{
|
||||
firstLockAccount = this;
|
||||
secondLockAccount = target;
|
||||
}
|
||||
else
|
||||
{
|
||||
firstLockAccount = target;
|
||||
secondLockAccount = this;
|
||||
}
|
||||
|
||||
firstLockAccount.lock.lock();
|
||||
try
|
||||
{
|
||||
secondLockAccount.lock.lock();
|
||||
try
|
||||
{
|
||||
this.balance -= amount;
|
||||
target.balance += amount;
|
||||
}
|
||||
finally
|
||||
{
|
||||
secondLockAccount.lock.unlock();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
firstLockAccount.lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user