Files
2026-06-08 16:09:57 +03:30

183 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
### ⚛️ Atomic Variables & Synchronization
1. Atomic variables in Java are part of the `java.util.concurrent.atomic` package.
They provide lockfree, threadsafe operations on a single variable.
Their purpose is to allow readmodifywrite operations to be performed
as a single, indivisible unit without using synchronized blocks or explicit locks.
2.
* `AtomicInteger` for `int` values
* `AtomicLong` for `long` values
* `AtomicBoolean` for `boolean` values
* `AtomicReference<V>` for object references (any type).
</br>
Typical use case for `AtomicInteger`: A request counter in a web server.
Each incoming request increments the counter to generate a unique ID or to count total requests.
Multiple threads (handling requests concurrently) can safely call `incrementAndGet()` without any locks.
3. Locks and atomic variables are both tools for managing thread safety,
but they differ significantly in how they work and when you should use them.
</br>
* Locks can protect arbitrarily large sections of code and can coordinate access to multiple
variables simultaneously. For example, if you need to update two bank accounts in a single atomic
operation, you must use a lock. Atomic variables, on the other hand, only cover a single variable.
You cannot atomically update two independent `AtomicInteger` objects together without additional synchronization.
* Locks are blocking when a thread cannot acquire a lock, it is suspended by the operating system and later
woken up, which causes context switches and overhead. Atomic variables are nonblocking; they use hardwarelevel
CompareAndSwap (CAS) instructions. If an atomic operation fails because another thread modified the variable,
it simply retries immediately (spins) without leaving the CPU.
* Because locks can be held while waiting for other locks, they can cause deadlocks if not used carefully.
Atomic variables never cause deadlocks because there is no waiting for locks each operation either
succeeds immediately or retries.
* For simple operations like incrementing a counter, atomic variables are usually much faster than locks,
especially when contention is low to moderate. However, under extremely high contention (many threads pounding
the same variable), atomic variables may suffer from excessive retry spinning, and a welltuned lock might perform
better. For long critical sections (e.g., many lines of code, I/O, or complex updates), locks are more efficient
because spinning would waste CPU cycles.
* **When is a lock a better choice?**
-When you need to atomically update multiple variables that belong together (e.g., transferring money between accounts).
-When the critical section is long or contains blocking operations (like network calls or file I/O).
-When you need explicit waiting and notification.
* **When is an atomic variable a better choice?**
-For simple, singlevariable operations such as counters, sequence generators, or status flags.
-When you want lockfree code that cannot deadlock.
-For highfrequency updates where lock overhead would become a bottleneck (e.g., statistics collection, request counters).
#### 🎯Bonus Task:
```java
public class raceCondition
{
private static int plainCounter = 0;
private static AtomicInteger atomicCounter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException
{
final int THREAD_COUNT = 10;
final int INCREMENTS_PER_THREAD = 1000;
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++)
{
threads[i] = new Thread(()-> {
for (int j = 0; j < INCREMENTS_PER_THREAD; j++)
{
plainCounter++;
atomicCounter.incrementAndGet();
}
});
}
for (int i = 0;i < THREAD_COUNT; i++)
{
threads[i].start();
}
for (int i = 0;i < THREAD_COUNT; i++)
{
threads[i].join();
}
int expected = THREAD_COUNT*INCREMENTS_PER_THREAD;
System.out.println("expected: "+expected);
System.out.println("int: "+plainCounter);
System.out.println("atomic: "+atomicCounter);
}
}
```
sample output:
```
expected: 10000
int: 8880
atomic: 10000
```
---
### 🔒 Locks & Concurrent Design
4. **Explanation:**
Even if a program has no race conditions, it can still suffer from poor performance when many threads compete
for the same resources. High contention means many threads try to access the same shared data or locks at the same time.
While correctness is preserved, throughput can drop dramatically because threads spend more time waiting, retrying,
or invalidating caches than doing useful work.
**Three concurrencyrelated factors that limit scalability:**
* **Lock contention**
If a program uses a single coarsegrained lock (e.g., synchronizing the whole method), only one thread can
execute the critical section at a time. All other threads queue up and block. As more threads are added, the queue
grows, but the throughput cannot exceed the rate at which the lock is released and reacquired. This turns a
concurrent program into essentially a sequential one for that resource, creating a scalability bottleneck.
* **Cache coherence traffic**
On modern multicore CPUs, each core has its own cache. When multiple threads repeatedly read and write to the
same memory location (even with atomic operations), the caches must stay consistent. The hardware uses a cache
coherence protocol. Every write to a shared variable invalidates the cache line in all other cores, forcing them to
reload from main memory or a shared cache. Under high contention, this causes a storm of invalidations and cache
misses, increasing memory latency and reducing performance even without explicit locks.
* **False sharing**
False sharing occurs when two or more threads modify different variables that happen to reside on the same
cache line (typically 64 bytes). Although the threads do not share the same logical variable, the cache coherence
protocol treats the entire line as shared. When one thread updates its variable, the cache line is invalidated on
other cores, causing unnecessary reloads. This can slow down seemingly independent threads, and the problem worsens
with more threads because the probability of cache line overlaps increases.
5. * **Context switching overhead**
The operating system can run only as many threads as there are hardware cores (or hardware threads like
HyperThreading). When the number of active threads exceeds the number of cores, the OS must constantly pause one thread
and switch to another. A context switch involves saving and restoring register states, updating memory management
structures, and flushing parts of the pipeline and caches. Each switch costs microseconds small per switch, but when
thousands of switches happen per second, total overhead becomes significant, reducing useful work throughput.
* **Contention for shared resources**
As more threads compete for the same locks, memory, or I/O channels, the fraction of time spent waiting
(blocking) increases. Throughput does not increase linearly and eventually saturates. Contention on a popular lock
can cause the system to spend most of its time in the operating system scheduler and in lockhandling code, leading
to severe performance collapse.
* **Cache coherence**
More threads mean more cores reading and writing to shared data. Every write to a shared variable triggers cache
coherency traffic (invalidations, bus transactions). This traffic increases with the square of the number of
contending cores in some cases. Moreover, all cores share the same memory bus. When many threads access memory
heavily, the bus becomes a bottleneck, and memory latency increases due to queuing delays.
* **Synchronization overhead**
Every locking operation (`synchronized`, `ReentrantLock`, `Semaphore`) involves overhead: acquiring the lock,
possibly parking the thread, and later unparking it. Even lockfree atomic operations under high contention cause
repeated retries, which burn CPU cycles without progressing. The overhead per operation grows, and total throughput can drop.
---
### ⚠️ Deadlocks
6. * **Why Deadlocks Often Appear Only in Production? (ThreadScheduling Perspective)**
Deadlocks are notoriously hard to reproduce during testing because they depend on specific interleavings of thread
execution the exact order in which threads acquire locks. In a testing environment (e.g., with low load, few cores, or
deterministic scheduling), the probability of hitting the exact timing window where two threads hold locks in opposite
order is extremely low.
* **Two Strategies to Expose Deadlocks During Testing:**
* Stress Testing with Thread Interleaving Controllers (e.g., jcstress, Lincheck):
Use tools that systematically explore thread interleavings. For example, Java Concurrency Stress (jcstress)
generates many schedules, including rare ones. Alternatively, ConcurrentLinkedDeque test harnesses or Lincheck
(from Kotlin) can be used. A simpler approach: in a test, repeatedly run a scenario with many threads and use
Thread.yield() or Thread.sleep(1) at strategic points to increase the chance of switching contexts in the middle of
lock acquisition.
* Inject Artificial Delays and Random Preemption Points:
Within the critical sections, insert small random sleeps (Thread.sleep(1)) or Thread.yield() right after
acquiring the first lock but before acquiring the second lock. This greatly increases the chance of interleaving.
Use a randomised test runner that loops the same test thousands of times with different random seeds. Also, run
tests on machines with more CPU cores and under load to make scheduling less predictable.