Many developers start learning concurrency by memorizing APIs directly.
For example:
- How to create a
Thread. - How to use
Runnable. - How to add
synchronized. - What is the difference between
ReentrantLockandsynchronized. - What is AQS.
However, this learning approach has a problem:
You may know how to use the APIs, but you do not understand why you need them in real business scenarios and how to choose the right solution.
Therefore, I believe the best way to learn concurrency is not starting from APIs.
Instead, we should start from problems.
1. Why Do We Need Concurrency?
Let's start with a question:
Why do programs need concurrency?
Assume we have an order system.
After a user submits an order, the system needs to perform several operations:
- Deduct inventory.
- Create the order.
- Process payment.
- Send notifications.
If everything is executed sequentially by a single thread, the workflow looks like this:
First:
Deduct inventory
After it finishes:
Create order
After it finishes:
Process payment
After it finishes:
Send notification
The total execution time of all steps becomes the response time of the entire request.
However, after analyzing the process carefully, we can find that some operations do not need to block the user.
For example:
- Sending SMS notifications.
- Recording operation logs.
- Updating statistics.
These operations do not affect whether the order is successfully created.
Therefore, we can move them to background threads and execute them asynchronously.
This is the first value of concurrency:
Improving system response time
Besides asynchronous execution, there is another important reason:
Making better use of multi-core CPUs
Modern servers may have:
- 8 CPU cores.
- 16 CPU cores.
- 32 CPU cores.
If an application only uses one thread, then effectively only one CPU core is doing the work.
If a task can be divided into multiple independent parts, multiple threads can execute them simultaneously.
Therefore, concurrency mainly solves two problems:
- Improve system responsiveness.
- Improve hardware resource utilization.
2. When Multiple Threads Run Together, What Problems Appear?
Many people think:
Multithreading simply means splitting tasks among multiple threads.
But things are not that simple.
The real challenge is:
What happens when multiple threads access shared data at the same time?
For example:
We have a variable:
int count = 0;
100 threads execute:
count++;
According to our expectation:
The final result should be:
100
However, the actual result may be:
98
99
or even lower.
Why?
Because:
count++ is not a single operation.
It actually contains three steps:
- Read
count. - Increment the value.
- Write the new value back.
Suppose two threads execute simultaneously:
Thread A reads:
count = 0
Thread B also reads:
count = 0
Then:
Thread A calculates:
0 + 1 = 1
Thread B calculates:
0 + 1 = 1
Finally, both write:
count = 1
One update is lost.
This is the first core problem in concurrency:
Atomicity
Atomicity means:
An operation should either completely execute or not execute at all.
It should not be interrupted halfway by another thread.
3. Visibility Problem
Besides atomicity, there is another issue:
Visibility
What does visibility mean?
Suppose:
Thread A modifies a variable.
When will Thread B see this modification?
Many people assume:
Once Thread A changes the value, Thread B immediately sees it.
But this is not always true.
To improve performance, modern CPUs use caches.
Threads may read values from their own CPU cache.
Therefore:
Thread A has already updated the value.
But Thread B still sees the old value.
This is the visibility problem.
4. Ordering Problem
The third problem is:
Instruction Ordering
When we write code:
a = 1;
b = 2;
We naturally assume execution happens from top to bottom.
However, for performance optimization:
- The compiler.
- The CPU.
may reorder instructions.
If there are no dependencies between instructions, usually there is no problem.
But when dependencies exist, reordering may cause unexpected behavior.
This is the ordering problem.
Therefore, Java concurrency has three fundamental problems:
The Three Core Problems
- Atomicity.
- Visibility.
- Ordering.
5. How Do We Solve Concurrency Problems?
After understanding the problems, let's look at solutions.
volatile
volatile mainly solves:
- Visibility.
- Ordering.
For example:
When one thread modifies a variable, other threads can immediately see the latest value.
However:
volatile does not guarantee atomicity.
For example:
count++;
Even with volatile, this operation is still not thread-safe.
synchronized
synchronized is the most commonly used lock mechanism in Java.
It solves:
- Atomicity.
- Visibility.
- Ordering.
The simple idea:
Only one thread can enter the synchronized section at the same time.
For example:
Multiple threads modify an account balance.
With locking:
Thread A
|
v
Update balance
Thread B waits
Thread B
|
v
Update balance
Although performance is reduced slightly, data correctness is guaranteed.
ReentrantLock
ReentrantLock is another locking mechanism.
Compared with synchronized, it provides more advanced features:
- Try acquiring a lock.
- Timeout when waiting for a lock.
- Interruptible lock acquisition.
However, another question appears:
When multiple threads compete for the same lock:
How do they wait and queue?
This leads us to:
AQS (AbstractQueuedSynchronizer)
AQS is the core foundation behind many Java synchronization tools.
It maintains a waiting queue.
Threads that fail to acquire a lock do not continuously consume CPU resources.
Instead:
They enter the waiting queue.
When the lock is released:
The next waiting thread is awakened.
This is the core design idea behind Java locks.
CAS (Compare And Swap)
Besides locks, another approach is:
CAS
The idea is:
Before modifying data:
First compare whether the current value is the expected value.
If yes:
Modify directly.
If no:
Someone else has modified the data.
Retry.
Java classes such as:
AtomicIntegerAtomicReference
use CAS internally.
Advantages:
- High performance.
- Lock-free design.
Disadvantages:
- Under heavy contention, repeated retries may occur.
6. How Do Threads Cooperate?
After solving thread safety issues, another question appears:
How do multiple threads cooperate to complete tasks?
Thread cooperation mainly includes several scenarios.
Why Do Threads Need to Wait?
Example:
A consumer thread.
If there are no messages:
Should it continuously check?
Like:
while(true){
checkMessage();
}
This wastes CPU.
A better approach:
The thread enters a waiting state.
When messages arrive:
It gets awakened.
Related mechanisms:
sleep
Meaning:
"I will pause for a certain amount of time."
wait
Meaning:
"I am waiting for a specific condition."
park
A lower-level thread blocking mechanism.
How Do Threads Communicate?
A simple approach:
Shared variables.
For example:
A volatile variable.
One thread updates the state.
Another thread reads the state.
Another mechanism:
wait() and notify().
One thread waits.
Another thread sends notification.
In real-world development, concurrent utilities are more commonly used.
CountDownLatch
Scenario:
One thread waits for multiple threads to finish.
Example:
System startup:
- Database initialization.
- Cache initialization.
- Message queue initialization.
Only after all complete:
The system starts.
CyclicBarrier
Scenario:
Multiple threads wait for each other.
Example:
A competition.
All participants finish preparation.
Then:
The competition starts.
Semaphore
Scenario:
Limit resource access.
Example:
Database connection pool.
Maximum:
100 connections
Extra requests must wait.
7. How Do Multiple Threads Complete Complex Tasks?
Modern systems often require multiple asynchronous operations.
Example:
Product detail page.
Need:
- Product information.
- Inventory information.
- Promotion information.
- User information.
These can be queried in parallel.
Then combine results.
Technologies:
- Callable.
- Future.
- FutureTask.
- CompletableFuture.
CompletableFuture supports:
- Parallel execution.
- Task dependency.
- Result combination.
- Exception handling.
8. How Should Threads Be Stopped?
Threads eventually need to finish.
Previously Java provided:
Thread.stop()
However, this approach is unsafe.
Because it may cause:
- Locks not being released.
- Data not being saved.
The recommended approach today:
interrupt()
interrupt() does not forcibly kill a thread.
Instead:
It sends a stop signal.
The thread decides when and how to exit safely.
This is called:
Graceful Shutdown
9. Too Many Threads: What Happens?
If every request creates a new thread:
What problems appear?
Threads are not free.
Creating threads requires:
- Memory.
- CPU scheduling.
- Context switching.
Too many threads may actually reduce performance.
Therefore:
We need:
Thread Pool
A thread pool is responsible for:
- Creating threads.
- Managing thread lifecycle.
- Reusing threads.
The core class:
ThreadPoolExecutor
It manages:
- Core thread number.
- Maximum thread number.
- Task queue.
- Rejection policy.
10. Concurrent Collections
Normal collections:
- HashMap.
- ArrayList.
are not thread-safe.
Java provides:
ConcurrentHashMap
It uses:
- CAS.
- Lock mechanisms.
to achieve high-performance concurrent access.
CopyOnWriteArrayList
Suitable for:
Read-heavy, write-light scenarios.
11. Why Do Threads Get Stuck?
The final question:
Why can threads stop making progress?
The classic problem:
Deadlock
Example:
Thread A:
Lock 1 acquired
Waiting for Lock 2
Thread B:
Lock 2 acquired
Waiting for Lock 1
Both wait forever.
Nobody can continue.
This is deadlock.
How to Avoid Deadlocks
Common approaches:
- Use consistent lock ordering.
- Reduce lock scope.
- Avoid nested locks.
- Use timeout-based lock acquisition.
Summary
Learning Java concurrency should not start from:
- Thread.
- Lock.
- ConcurrentHashMap.
The correct learning path is:
Why do we need concurrency?
↓
How do threads execute?
↓
Why do multiple threads cause problems?
↓
Atomicity, Visibility, Ordering
↓
How to guarantee thread safety?
↓
volatile, synchronized, CAS, AQS
↓
How do threads wait and communicate?
↓
How do threads cooperate?
↓
How to stop threads safely?
↓
How thread pools manage execution?
↓
Concurrent collections and asynchronous programming
The real purpose of concurrency programming is:
When multiple execution flows run simultaneously, how do we guarantee correctness while improving system performance?
This is the concurrency mindset that a senior Java engineer truly needs to understand.
GitHub:
github.com/markliu2013/...
Top comments (0)